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
6 changes: 6 additions & 0 deletions wasmtime-environ/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ lazy_static = "1.3.0"
spin = "0.5.0"
log = { version = "0.4.8", default-features = false }

[dev-dependencies]
tempfile = "3"
target-lexicon = { version = "0.4.0", default-features = false }
pretty_env_logger = "0.3.0"
rand = { version = "0.7.0", features = ["small_rng"] }

[features]
default = ["std"]
std = ["cranelift-codegen/std", "cranelift-wasm/std"]
Expand Down
4 changes: 2 additions & 2 deletions wasmtime-environ/src/address_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use std::vec::Vec;

/// Single source location to generated address mapping.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct InstructionAddressMap {
/// Original source location.
pub srcloc: ir::SourceLoc,
Expand All @@ -21,7 +21,7 @@ pub struct InstructionAddressMap {
}

/// Function and its instructions addresses mappings.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct FunctionAddressMap {
/// Instructions maps.
/// The array is sorted by the InstructionAddressMap::code_offset field.
Expand Down
5 changes: 4 additions & 1 deletion wasmtime-environ/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ pub struct ModuleCacheEntry {
mod_cache_path: Option<PathBuf>,
}

#[derive(serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct ModuleCacheData {
compilation: Compilation,
relocations: Relocations,
Expand Down Expand Up @@ -505,3 +505,6 @@ impl<'de> Deserialize<'de> for JtOffsetsWrapper<'_> {
deserializer.deserialize_seq(JtOffsetsWrapperVisitor {})
}
}

#[cfg(test)]
mod tests;
325 changes: 325 additions & 0 deletions wasmtime-environ/src/cache/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
use super::*;
use crate::address_map::{FunctionAddressMap, InstructionAddressMap};
use crate::compilation::{Relocation, RelocationTarget};
use crate::module::{MemoryPlan, MemoryStyle, Module};
use cranelift_codegen::ir::entities::JumpTable;
use cranelift_codegen::{binemit, ir, isa, settings, ValueLocRange};
use cranelift_entity::EntityRef;
use cranelift_entity::{PrimaryMap, SecondaryMap};
use cranelift_wasm::{DefinedFuncIndex, FuncIndex, Global, GlobalInit, Memory, SignatureIndex};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use std::boxed::Box;
use std::cmp::{max, min};
use std::fs;
use std::str::FromStr;
use std::vec::Vec;
use target_lexicon::triple;
use tempfile;

// Since cache system is a global thing, each test needs to be run in seperate process.
// So, init() tests are run as integration tests.
// However, caching is a private thing, an implementation detail, and needs to be tested
// from the inside of the module. Thus we have one big test here.

#[test]
fn test_write_read_cache() {
pretty_env_logger::init();
let dir = tempfile::tempdir().expect("Can't create temporary directory");
conf::init(true, Some(dir.path()));
assert!(conf::cache_enabled());
// assumption: config init creates cache directory and returns canonicalized path
assert_eq!(
*conf::cache_directory(),
fs::canonicalize(dir.path()).unwrap()
);

let mut rng = SmallRng::from_seed([
0x42, 0x04, 0xF3, 0x44, 0x11, 0x22, 0x33, 0x44, 0x67, 0x68, 0xFF, 0x00, 0x44, 0x23, 0x7F,
0x96,
]);

let mut code_container = Vec::new();
code_container.resize(0x4000, 0);
rng.fill(&mut code_container[..]);

let isa1 = new_isa("riscv64");
let isa2 = new_isa("i386");
let module1 = new_module(&mut rng);
let module2 = new_module(&mut rng);
let function_body_inputs1 = new_function_body_inputs(&mut rng, &code_container);
let function_body_inputs2 = new_function_body_inputs(&mut rng, &code_container);
let compiler1 = "test-1";
let compiler2 = "test-2";

let entry1 = ModuleCacheEntry::new(&module1, &function_body_inputs1, &*isa1, compiler1, false);
assert!(entry1.mod_cache_path().is_some());
assert!(entry1.get_data().is_none());
let data1 = new_module_cache_data(&mut rng);
entry1.update_data(&data1);
assert_eq!(entry1.get_data().expect("Cache should be available"), data1);

let entry2 = ModuleCacheEntry::new(&module2, &function_body_inputs1, &*isa1, compiler1, false);
let data2 = new_module_cache_data(&mut rng);
entry2.update_data(&data2);
assert_eq!(entry1.get_data().expect("Cache should be available"), data1);
assert_eq!(entry2.get_data().expect("Cache should be available"), data2);

let entry3 = ModuleCacheEntry::new(&module1, &function_body_inputs2, &*isa1, compiler1, false);
let data3 = new_module_cache_data(&mut rng);
entry3.update_data(&data3);
assert_eq!(entry1.get_data().expect("Cache should be available"), data1);
assert_eq!(entry2.get_data().expect("Cache should be available"), data2);
assert_eq!(entry3.get_data().expect("Cache should be available"), data3);

let entry4 = ModuleCacheEntry::new(&module1, &function_body_inputs1, &*isa2, compiler1, false);
let data4 = new_module_cache_data(&mut rng);
entry4.update_data(&data4);
assert_eq!(entry1.get_data().expect("Cache should be available"), data1);
assert_eq!(entry2.get_data().expect("Cache should be available"), data2);
assert_eq!(entry3.get_data().expect("Cache should be available"), data3);
assert_eq!(entry4.get_data().expect("Cache should be available"), data4);

let entry5 = ModuleCacheEntry::new(&module1, &function_body_inputs1, &*isa1, compiler2, false);
let data5 = new_module_cache_data(&mut rng);
entry5.update_data(&data5);
assert_eq!(entry1.get_data().expect("Cache should be available"), data1);
assert_eq!(entry2.get_data().expect("Cache should be available"), data2);
assert_eq!(entry3.get_data().expect("Cache should be available"), data3);
assert_eq!(entry4.get_data().expect("Cache should be available"), data4);
assert_eq!(entry5.get_data().expect("Cache should be available"), data5);

let data6 = new_module_cache_data(&mut rng);
entry1.update_data(&data6);
assert_eq!(entry1.get_data().expect("Cache should be available"), data6);
assert_eq!(entry2.get_data().expect("Cache should be available"), data2);
assert_eq!(entry3.get_data().expect("Cache should be available"), data3);
assert_eq!(entry4.get_data().expect("Cache should be available"), data4);
assert_eq!(entry5.get_data().expect("Cache should be available"), data5);

assert!(data1 != data2 && data1 != data3 && data1 != data4 && data1 != data5 && data1 != data6);
}

fn new_isa(name: &str) -> Box<dyn isa::TargetIsa> {
let shared_builder = settings::builder();
let shared_flags = settings::Flags::new(shared_builder);
isa::lookup(triple!(name))
.expect("can't find specified isa")
.finish(shared_flags)
}

fn new_module(rng: &mut impl Rng) -> Module {
// There are way too many fields. Just fill in some of them.
let mut m = Module::new();

if rng.gen_bool(0.5) {
m.signatures.push(ir::Signature {
params: vec![],
returns: vec![],
call_conv: isa::CallConv::Fast,
});
}

for i in 0..rng.gen_range(1, 0x8) {
m.functions.push(SignatureIndex::new(i));
}

if rng.gen_bool(0.8) {
m.memory_plans.push(MemoryPlan {
memory: Memory {
minimum: rng.gen(),
maximum: rng.gen(),
shared: rng.gen(),
},
style: MemoryStyle::Dynamic,
offset_guard_size: rng.gen(),
});
}

if rng.gen_bool(0.4) {
m.globals.push(Global {
ty: ir::Type::int(16).unwrap(),
mutability: rng.gen(),
initializer: GlobalInit::I32Const(rng.gen()),
});
}

m
}

fn new_function_body_inputs<'data>(
rng: &mut impl Rng,
code_container: &'data Vec<u8>,
) -> PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>> {
let len = code_container.len();
let mut pos = rng.gen_range(0, code_container.len());
(2..rng.gen_range(4, 14))
.map(|j| {
let (old_pos, end) = (pos, min(pos + rng.gen_range(0x10, 0x200), len));
pos = end % len;
FunctionBodyData {
data: &code_container[old_pos..end],
module_offset: (rng.next_u64() + j) as usize,
}
})
.collect()
}

fn new_module_cache_data(rng: &mut impl Rng) -> ModuleCacheData {
// WARNING: if method changed, update PartialEq impls below, too!
let funcs = (0..rng.gen_range(0, 10))
.map(|i| {
let mut sm = SecondaryMap::new(); // doesn't implement from iterator
sm.resize(i as usize * 2);
sm.values_mut().enumerate().for_each(|(j, v)| {
if rng.gen_bool(0.33) {
*v = (j as u32) * 3 / 4
}
});
CodeAndJTOffsets {
body: (0..(i * 3 / 2)).collect(),
jt_offsets: sm,
}
})
.collect();

let relocs = (0..rng.gen_range(1, 0x10))
.map(|i| {
vec![
Relocation {
reloc: binemit::Reloc::X86CallPCRel4,
reloc_target: RelocationTarget::UserFunc(FuncIndex::new(i as usize * 42)),
offset: i + rng.next_u32(),
addend: 0,
},
Relocation {
reloc: binemit::Reloc::Arm32Call,
reloc_target: RelocationTarget::LibCall(ir::LibCall::CeilF64),
offset: rng.gen_range(4, i + 55),
addend: (42 * i) as i64,
},
]
})
.collect();

let trans = (4..rng.gen_range(4, 0x10))
.map(|i| FunctionAddressMap {
instructions: vec![InstructionAddressMap {
srcloc: ir::SourceLoc::new(rng.gen()),
code_offset: rng.gen(),
code_len: i,
}],
start_srcloc: ir::SourceLoc::new(rng.gen()),
end_srcloc: ir::SourceLoc::new(rng.gen()),
body_offset: rng.gen(),
body_len: 0x31337,
})
.collect();

let value_ranges = (4..rng.gen_range(4, 0x10))
.map(|i| {
(i..i + rng.gen_range(4, 8))
.map(|k| {
(
ir::ValueLabel::new(k),
(0..rng.gen_range(0, 4))
.map(|_| ValueLocRange {
loc: ir::ValueLoc::Reg(rng.gen()),
start: rng.gen(),
end: rng.gen(),
})
.collect(),
)
})
.collect()
})
.collect();

let stack_slots = (0..rng.gen_range(0, 0x6))
.map(|_| {
let mut slots = ir::StackSlots::new();
slots.push(ir::StackSlotData {
kind: ir::StackSlotKind::SpillSlot,
size: rng.gen(),
offset: rng.gen(),
});
slots.frame_size = rng.gen();
slots
})
.collect();

ModuleCacheData::from_tuple((
Compilation::new(funcs),
relocs,
trans,
value_ranges,
stack_slots,
))
}

impl ModuleCacheEntry {
pub fn mod_cache_path(&self) -> &Option<PathBuf> {
&self.mod_cache_path
}
}

// cranelift's types (including SecondaryMap) doesn't implement PartialEq
impl PartialEq for ModuleCacheData {
fn eq(&self, other: &Self) -> bool {
// compilation field
if self.compilation.len() != other.compilation.len() {
return false;
}

for i in (0..self.compilation.len()).map(DefinedFuncIndex::new) {
let lhs = self.compilation.get(i);
let rhs = other.compilation.get(i);
if lhs.body != rhs.body || lhs.jt_offsets.get_default() != rhs.jt_offsets.get_default()
{
return false;
}

for j in (0..max(lhs.jt_offsets.len(), rhs.jt_offsets.len())).map(JumpTable::new) {
if lhs.jt_offsets.get(j) != rhs.jt_offsets.get(j) {
return false;
}
}
}

// relocs
{
if self.relocations.len() != other.relocations.len() {
return false;
}
let it_lhs = self.relocations.iter();
let it_rhs = other.relocations.iter();
if it_lhs.zip(it_rhs).any(|(lhs, rhs)| lhs != rhs) {
return false;
}
}

// debug symbols
{
if self.address_transforms.len() != other.address_transforms.len() {
return false;
}
let it_lhs = self.address_transforms.iter();
let it_rhs = other.address_transforms.iter();
if it_lhs.zip(it_rhs).any(|(lhs, rhs)| lhs != rhs) {
return false;
}
}

true
}
}

// binemit::Reloc doesn't implement PartialEq
impl PartialEq for Relocation {
fn eq(&self, other: &Self) -> bool {
self.reloc as u64 == other.reloc as u64
&& self.reloc_target == other.reloc_target
&& self.offset == other.offset
&& self.addend == other.addend
}
}
2 changes: 1 addition & 1 deletion wasmtime-environ/src/compilation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pub struct Relocation {
}

/// Destination function. Can be either user function or some special one, like `memory.grow`.
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
pub enum RelocationTarget {
/// The user function index.
UserFunc(FuncIndex),
Expand Down
10 changes: 10 additions & 0 deletions wasmtime-environ/tests/cache_fail_calling_init_twice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use tempfile;
use wasmtime_environ::cache_conf;

#[test]
#[should_panic]
fn test_fail_calling_init_twice() {
let dir = tempfile::tempdir().expect("Can't create temporary directory");
cache_conf::init(true, Some(dir.path()));
cache_conf::init(true, Some(dir.path()));
}
Loading