Initial reorg.
This is largely the same as #305, but updated for the current tree.
This commit is contained in:
59
crates/environ/src/address_map.rs
Normal file
59
crates/environ/src/address_map.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Data structures to provide transformation of the source
|
||||
// addresses of a WebAssembly module into the native code.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::DefinedFuncIndex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Single source location to generated address mapping.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InstructionAddressMap {
|
||||
/// Original source location.
|
||||
pub srcloc: ir::SourceLoc,
|
||||
|
||||
/// Generated instructions offset.
|
||||
pub code_offset: usize,
|
||||
|
||||
/// Generated instructions length.
|
||||
pub code_len: usize,
|
||||
}
|
||||
|
||||
/// Function and its instructions addresses mappings.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FunctionAddressMap {
|
||||
/// Instructions maps.
|
||||
/// The array is sorted by the InstructionAddressMap::code_offset field.
|
||||
pub instructions: Vec<InstructionAddressMap>,
|
||||
|
||||
/// Function start source location (normally declaration).
|
||||
pub start_srcloc: ir::SourceLoc,
|
||||
|
||||
/// Function end source location.
|
||||
pub end_srcloc: ir::SourceLoc,
|
||||
|
||||
/// Generated function body offset if applicable, otherwise 0.
|
||||
pub body_offset: usize,
|
||||
|
||||
/// Generated function body length.
|
||||
pub body_len: usize,
|
||||
}
|
||||
|
||||
/// Module functions addresses mappings.
|
||||
pub type ModuleAddressMap = PrimaryMap<DefinedFuncIndex, FunctionAddressMap>;
|
||||
|
||||
/// Value ranges for functions.
|
||||
pub type ValueLabelsRanges = PrimaryMap<DefinedFuncIndex, cranelift_codegen::ValueLabelsRanges>;
|
||||
|
||||
/// Stack slots for functions.
|
||||
pub type StackSlots = PrimaryMap<DefinedFuncIndex, ir::StackSlots>;
|
||||
|
||||
/// Module `vmctx` related info.
|
||||
pub struct ModuleVmctxInfo {
|
||||
/// The memory definition offset in the VMContext structure.
|
||||
pub memory_offset: i64,
|
||||
|
||||
/// The functions stack slots.
|
||||
pub stack_slots: StackSlots,
|
||||
}
|
||||
295
crates/environ/src/cache.rs
Normal file
295
crates/environ/src/cache.rs
Normal file
@@ -0,0 +1,295 @@
|
||||
use crate::address_map::{ModuleAddressMap, ValueLabelsRanges};
|
||||
use crate::compilation::{Compilation, Relocations, Traps};
|
||||
use crate::module::Module;
|
||||
use crate::module_environ::FunctionBodyData;
|
||||
use alloc::string::{String, ToString};
|
||||
use core::hash::Hasher;
|
||||
use cranelift_codegen::{ir, isa};
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::DefinedFuncIndex;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[macro_use] // for tests
|
||||
mod config;
|
||||
mod worker;
|
||||
|
||||
use config::{cache_config, CacheConfig};
|
||||
pub use config::{create_new_config, init};
|
||||
use worker::{worker, Worker};
|
||||
|
||||
lazy_static! {
|
||||
static ref SELF_MTIME: String = {
|
||||
std::env::current_exe()
|
||||
.map_err(|_| warn!("Failed to get path of current executable"))
|
||||
.ok()
|
||||
.and_then(|path| {
|
||||
fs::metadata(&path)
|
||||
.map_err(|_| warn!("Failed to get metadata of current executable"))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|metadata| {
|
||||
metadata
|
||||
.modified()
|
||||
.map_err(|_| warn!("Failed to get metadata of current executable"))
|
||||
.ok()
|
||||
})
|
||||
.map(|mtime| match mtime.duration_since(std::time::UNIX_EPOCH) {
|
||||
Ok(duration) => format!("{}", duration.as_millis()),
|
||||
Err(err) => format!("m{}", err.duration().as_millis()),
|
||||
})
|
||||
.unwrap_or_else(|| "no-mtime".to_string())
|
||||
};
|
||||
}
|
||||
|
||||
pub struct ModuleCacheEntry<'config, 'worker>(Option<ModuleCacheEntryInner<'config, 'worker>>);
|
||||
|
||||
struct ModuleCacheEntryInner<'config, 'worker> {
|
||||
mod_cache_path: PathBuf,
|
||||
cache_config: &'config CacheConfig,
|
||||
worker: &'worker Worker,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct ModuleCacheData {
|
||||
compilation: Compilation,
|
||||
relocations: Relocations,
|
||||
address_transforms: ModuleAddressMap,
|
||||
value_ranges: ValueLabelsRanges,
|
||||
stack_slots: PrimaryMap<DefinedFuncIndex, ir::StackSlots>,
|
||||
traps: Traps,
|
||||
}
|
||||
|
||||
type ModuleCacheDataTupleType = (
|
||||
Compilation,
|
||||
Relocations,
|
||||
ModuleAddressMap,
|
||||
ValueLabelsRanges,
|
||||
PrimaryMap<DefinedFuncIndex, ir::StackSlots>,
|
||||
Traps,
|
||||
);
|
||||
|
||||
struct Sha256Hasher(Sha256);
|
||||
|
||||
impl<'config, 'worker> ModuleCacheEntry<'config, 'worker> {
|
||||
pub fn new<'data>(
|
||||
module: &Module,
|
||||
function_body_inputs: &PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
compiler_name: &str,
|
||||
generate_debug_info: bool,
|
||||
) -> Self {
|
||||
let cache_config = cache_config();
|
||||
if cache_config.enabled() {
|
||||
Self(Some(ModuleCacheEntryInner::new(
|
||||
module,
|
||||
function_body_inputs,
|
||||
isa,
|
||||
compiler_name,
|
||||
generate_debug_info,
|
||||
cache_config,
|
||||
worker(),
|
||||
)))
|
||||
} else {
|
||||
Self(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn from_inner<'data>(inner: ModuleCacheEntryInner<'config, 'worker>) -> Self {
|
||||
Self(Some(inner))
|
||||
}
|
||||
|
||||
pub fn get_data(&self) -> Option<ModuleCacheData> {
|
||||
if let Some(inner) = &self.0 {
|
||||
inner.get_data().map(|val| {
|
||||
inner.worker.on_cache_get_async(&inner.mod_cache_path); // call on success
|
||||
val
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_data(&self, data: &ModuleCacheData) {
|
||||
if let Some(inner) = &self.0 {
|
||||
inner.update_data(data).map(|val| {
|
||||
inner.worker.on_cache_update_async(&inner.mod_cache_path); // call on success
|
||||
val
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'config, 'worker> ModuleCacheEntryInner<'config, 'worker> {
|
||||
fn new<'data>(
|
||||
module: &Module,
|
||||
function_body_inputs: &PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
compiler_name: &str,
|
||||
generate_debug_info: bool,
|
||||
cache_config: &'config CacheConfig,
|
||||
worker: &'worker Worker,
|
||||
) -> Self {
|
||||
let hash = Sha256Hasher::digest(module, function_body_inputs);
|
||||
let compiler_dir = if cfg!(debug_assertions) {
|
||||
format!(
|
||||
"{comp_name}-{comp_ver}-{comp_mtime}",
|
||||
comp_name = compiler_name,
|
||||
comp_ver = env!("GIT_REV"),
|
||||
comp_mtime = *SELF_MTIME,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{comp_name}-{comp_ver}",
|
||||
comp_name = compiler_name,
|
||||
comp_ver = env!("GIT_REV"),
|
||||
)
|
||||
};
|
||||
let mod_filename = format!(
|
||||
"mod-{mod_hash}{mod_dbg}",
|
||||
mod_hash = base64::encode_config(&hash, base64::URL_SAFE_NO_PAD), // standard encoding uses '/' which can't be used for filename
|
||||
mod_dbg = if generate_debug_info { ".d" } else { "" },
|
||||
);
|
||||
let mod_cache_path = cache_config
|
||||
.directory()
|
||||
.join(isa.triple().to_string())
|
||||
.join(compiler_dir)
|
||||
.join(mod_filename);
|
||||
|
||||
Self {
|
||||
mod_cache_path,
|
||||
cache_config,
|
||||
worker,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_data(&self) -> Option<ModuleCacheData> {
|
||||
trace!("get_data() for path: {}", self.mod_cache_path.display());
|
||||
let compressed_cache_bytes = fs::read(&self.mod_cache_path).ok()?;
|
||||
let cache_bytes = zstd::decode_all(&compressed_cache_bytes[..])
|
||||
.map_err(|err| warn!("Failed to decompress cached code: {}", err))
|
||||
.ok()?;
|
||||
bincode::deserialize(&cache_bytes[..])
|
||||
.map_err(|err| warn!("Failed to deserialize cached code: {}", err))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn update_data(&self, data: &ModuleCacheData) -> Option<()> {
|
||||
trace!("update_data() for path: {}", self.mod_cache_path.display());
|
||||
let serialized_data = bincode::serialize(&data)
|
||||
.map_err(|err| warn!("Failed to serialize cached code: {}", err))
|
||||
.ok()?;
|
||||
let compressed_data = zstd::encode_all(
|
||||
&serialized_data[..],
|
||||
self.cache_config.baseline_compression_level(),
|
||||
)
|
||||
.map_err(|err| warn!("Failed to compress cached code: {}", err))
|
||||
.ok()?;
|
||||
|
||||
// Optimize syscalls: first, try writing to disk. It should succeed in most cases.
|
||||
// Otherwise, try creating the cache directory and retry writing to the file.
|
||||
if fs_write_atomic(&self.mod_cache_path, "mod", &compressed_data) {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Attempting to create the cache directory, because \
|
||||
failed to write cached code to disk, path: {}",
|
||||
self.mod_cache_path.display(),
|
||||
);
|
||||
|
||||
let cache_dir = self.mod_cache_path.parent().unwrap();
|
||||
fs::create_dir_all(cache_dir)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to create cache directory, path: {}, message: {}",
|
||||
cache_dir.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if fs_write_atomic(&self.mod_cache_path, "mod", &compressed_data) {
|
||||
Some(())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModuleCacheData {
|
||||
pub fn from_tuple(data: ModuleCacheDataTupleType) -> Self {
|
||||
Self {
|
||||
compilation: data.0,
|
||||
relocations: data.1,
|
||||
address_transforms: data.2,
|
||||
value_ranges: data.3,
|
||||
stack_slots: data.4,
|
||||
traps: data.5,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_tuple(self) -> ModuleCacheDataTupleType {
|
||||
(
|
||||
self.compilation,
|
||||
self.relocations,
|
||||
self.address_transforms,
|
||||
self.value_ranges,
|
||||
self.stack_slots,
|
||||
self.traps,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sha256Hasher {
|
||||
pub fn digest<'data>(
|
||||
module: &Module,
|
||||
function_body_inputs: &PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
) -> [u8; 32] {
|
||||
let mut hasher = Self(Sha256::new());
|
||||
module.hash_for_cache(function_body_inputs, &mut hasher);
|
||||
hasher.0.result().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hasher for Sha256Hasher {
|
||||
fn finish(&self) -> u64 {
|
||||
panic!("Sha256Hasher doesn't support finish!");
|
||||
}
|
||||
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
self.0.input(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Assumption: path inside cache directory.
|
||||
// Then, we don't have to use sound OS-specific exclusive file access.
|
||||
// Note: there's no need to remove temporary file here - cleanup task will do it later.
|
||||
fn fs_write_atomic(path: &Path, reason: &str, contents: &[u8]) -> bool {
|
||||
let lock_path = path.with_extension(format!("wip-atomic-write-{}", reason));
|
||||
fs::OpenOptions::new()
|
||||
.create_new(true) // atomic file creation (assumption: no one will open it without this flag)
|
||||
.write(true)
|
||||
.open(&lock_path)
|
||||
.and_then(|mut file| file.write_all(contents))
|
||||
// file should go out of scope and be closed at this point
|
||||
.and_then(|()| fs::rename(&lock_path, &path)) // atomic file rename
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to write file with rename, lock path: {}, target path: {}, err: {}",
|
||||
lock_path.display(),
|
||||
path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
625
crates/environ/src/cache/config.rs
vendored
Normal file
625
crates/environ/src/cache/config.rs
vendored
Normal file
@@ -0,0 +1,625 @@
|
||||
//! Module for configuring the cache system.
|
||||
|
||||
use super::worker;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use core::time::Duration;
|
||||
use directories::ProjectDirs;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, error, trace, warn};
|
||||
use serde::{
|
||||
de::{self, Deserializer},
|
||||
Deserialize,
|
||||
};
|
||||
use spin::Once;
|
||||
use std::fmt::Debug;
|
||||
use std::fs;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
// wrapped, so we have named section in config,
|
||||
// also, for possible future compatibility
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Config {
|
||||
cache: CacheConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CacheConfig {
|
||||
#[serde(skip)]
|
||||
errors: Vec<String>,
|
||||
|
||||
enabled: bool,
|
||||
directory: Option<PathBuf>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "worker-event-queue-size",
|
||||
deserialize_with = "deserialize_si_prefix"
|
||||
)]
|
||||
worker_event_queue_size: Option<u64>,
|
||||
#[serde(rename = "baseline-compression-level")]
|
||||
baseline_compression_level: Option<i32>,
|
||||
#[serde(rename = "optimized-compression-level")]
|
||||
optimized_compression_level: Option<i32>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "optimized-compression-usage-counter-threshold",
|
||||
deserialize_with = "deserialize_si_prefix"
|
||||
)]
|
||||
optimized_compression_usage_counter_threshold: Option<u64>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "cleanup-interval",
|
||||
deserialize_with = "deserialize_duration"
|
||||
)]
|
||||
cleanup_interval: Option<Duration>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "optimizing-compression-task-timeout",
|
||||
deserialize_with = "deserialize_duration"
|
||||
)]
|
||||
optimizing_compression_task_timeout: Option<Duration>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "allowed-clock-drift-for-files-from-future",
|
||||
deserialize_with = "deserialize_duration"
|
||||
)]
|
||||
allowed_clock_drift_for_files_from_future: Option<Duration>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "file-count-soft-limit",
|
||||
deserialize_with = "deserialize_si_prefix"
|
||||
)]
|
||||
file_count_soft_limit: Option<u64>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "files-total-size-soft-limit",
|
||||
deserialize_with = "deserialize_disk_space"
|
||||
)]
|
||||
files_total_size_soft_limit: Option<u64>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "file-count-limit-percent-if-deleting",
|
||||
deserialize_with = "deserialize_percent"
|
||||
)]
|
||||
file_count_limit_percent_if_deleting: Option<u8>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "files-total-size-limit-percent-if-deleting",
|
||||
deserialize_with = "deserialize_percent"
|
||||
)]
|
||||
files_total_size_limit_percent_if_deleting: Option<u8>,
|
||||
}
|
||||
|
||||
// Private static, so only internal function can access it.
|
||||
static CONFIG: Once<CacheConfig> = Once::new();
|
||||
static INIT_CALLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Returns cache configuration.
|
||||
///
|
||||
/// If system has not been initialized, it disables it.
|
||||
/// You mustn't call init() after it.
|
||||
pub fn cache_config() -> &'static CacheConfig {
|
||||
CONFIG.call_once(CacheConfig::new_cache_disabled)
|
||||
}
|
||||
|
||||
/// Initializes the cache system. Should be called exactly once,
|
||||
/// and before using the cache system. Otherwise it can panic.
|
||||
/// Returns list of errors. If empty, initialization succeeded.
|
||||
pub fn init<P: AsRef<Path> + Debug>(
|
||||
enabled: bool,
|
||||
config_file: Option<P>,
|
||||
init_file_per_thread_logger: Option<&'static str>,
|
||||
) -> &'static Vec<String> {
|
||||
INIT_CALLED
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.expect("Cache system init must be called at most once");
|
||||
assert!(
|
||||
CONFIG.r#try().is_none(),
|
||||
"Cache system init must be called before using the system."
|
||||
);
|
||||
let conf_file_str = format!("{:?}", config_file);
|
||||
let conf = CONFIG.call_once(|| CacheConfig::from_file(enabled, config_file));
|
||||
if conf.errors.is_empty() {
|
||||
if conf.enabled() {
|
||||
worker::init(init_file_per_thread_logger);
|
||||
}
|
||||
debug!("Cache init(\"{}\"): {:#?}", conf_file_str, conf)
|
||||
} else {
|
||||
error!(
|
||||
"Cache init(\"{}\"): errors: {:#?}",
|
||||
conf_file_str, conf.errors,
|
||||
)
|
||||
}
|
||||
&conf.errors
|
||||
}
|
||||
|
||||
/// Creates a new configuration file at specified path, or default path if None is passed.
|
||||
/// Fails if file already exists.
|
||||
pub fn create_new_config<P: AsRef<Path> + Debug>(
|
||||
config_file: Option<P>,
|
||||
) -> Result<PathBuf, String> {
|
||||
trace!("Creating new config file, path: {:?}", config_file);
|
||||
|
||||
let config_file = config_file.as_ref().map_or_else(
|
||||
|| DEFAULT_CONFIG_PATH.as_ref().map(|p| p.as_ref()),
|
||||
|p| Ok(p.as_ref()),
|
||||
)?;
|
||||
|
||||
if config_file.exists() {
|
||||
Err(format!(
|
||||
"Specified config file already exists! Path: {}",
|
||||
config_file.display()
|
||||
))?;
|
||||
}
|
||||
|
||||
let parent_dir = config_file
|
||||
.parent()
|
||||
.ok_or_else(|| format!("Invalid cache config path: {}", config_file.display()))?;
|
||||
|
||||
fs::create_dir_all(parent_dir).map_err(|err| {
|
||||
format!(
|
||||
"Failed to create config directory, config path: {}, error: {}",
|
||||
config_file.display(),
|
||||
err
|
||||
)
|
||||
})?;
|
||||
|
||||
let content = "\
|
||||
# Comment out certain settings to use default values.
|
||||
# For more settings, please refer to the documentation:
|
||||
# https://github.com/CraneStation/wasmtime/blob/master/CACHE_CONFIGURATION.md
|
||||
|
||||
[cache]
|
||||
enabled = true
|
||||
";
|
||||
|
||||
fs::write(&config_file, &content).map_err(|err| {
|
||||
format!(
|
||||
"Failed to flush config to the disk, path: {}, msg: {}",
|
||||
config_file.display(),
|
||||
err
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(config_file.to_path_buf())
|
||||
}
|
||||
|
||||
// permitted levels from: https://docs.rs/zstd/0.4.28+zstd.1.4.3/zstd/stream/write/struct.Encoder.html
|
||||
const ZSTD_COMPRESSION_LEVELS: std::ops::RangeInclusive<i32> = 0..=21;
|
||||
lazy_static! {
|
||||
static ref PROJECT_DIRS: Option<ProjectDirs> =
|
||||
ProjectDirs::from("", "CraneStation", "wasmtime");
|
||||
static ref DEFAULT_CONFIG_PATH: Result<PathBuf, String> = PROJECT_DIRS
|
||||
.as_ref()
|
||||
.map(|proj_dirs| proj_dirs.config_dir().join("wasmtime-cache-config.toml"))
|
||||
.ok_or_else(|| "Config file not specified and failed to get the default".to_string());
|
||||
}
|
||||
|
||||
// Default settings, you're welcome to tune them!
|
||||
// TODO: what do we want to warn users about?
|
||||
|
||||
// At the moment of writing, the modules couldn't depend on anothers,
|
||||
// so we have at most one module per wasmtime instance
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_WORKER_EVENT_QUEUE_SIZE: u64 = 0x10;
|
||||
const WORKER_EVENT_QUEUE_SIZE_WARNING_TRESHOLD: u64 = 3;
|
||||
// should be quick and provide good enough compression
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_BASELINE_COMPRESSION_LEVEL: i32 = zstd::DEFAULT_COMPRESSION_LEVEL;
|
||||
// should provide significantly better compression than baseline
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_OPTIMIZED_COMPRESSION_LEVEL: i32 = 20;
|
||||
// shouldn't be to low to avoid recompressing too many files
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_OPTIMIZED_COMPRESSION_USAGE_COUNTER_THRESHOLD: u64 = 0x100;
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_OPTIMIZING_COMPRESSION_TASK_TIMEOUT: Duration = Duration::from_secs(30 * 60);
|
||||
// the default assumes problems with timezone configuration on network share + some clock drift
|
||||
// please notice 24 timezones = max 23h difference between some of them
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_ALLOWED_CLOCK_DRIFT_FOR_FILES_FROM_FUTURE: Duration =
|
||||
Duration::from_secs(60 * 60 * 24);
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_FILE_COUNT_SOFT_LIMIT: u64 = 0x10_000;
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_FILES_TOTAL_SIZE_SOFT_LIMIT: u64 = 1024 * 1024 * 512;
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_FILE_COUNT_LIMIT_PERCENT_IF_DELETING: u8 = 70;
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_FILES_TOTAL_SIZE_LIMIT_PERCENT_IF_DELETING: u8 = 70;
|
||||
|
||||
// Deserializers of our custom formats
|
||||
// can be replaced with const generics later
|
||||
macro_rules! generate_deserializer {
|
||||
($name:ident($numname:ident: $numty:ty, $unitname:ident: &str) -> $retty:ty {$body:expr}) => {
|
||||
fn $name<'de, D>(deserializer: D) -> Result<$retty, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let text = Option::<String>::deserialize(deserializer)?;
|
||||
let text = match text {
|
||||
None => return Ok(None),
|
||||
Some(text) => text,
|
||||
};
|
||||
let text = text.trim();
|
||||
let split_point = text.find(|c: char| !c.is_numeric());
|
||||
let (num, unit) = split_point.map_or_else(|| (text, ""), |p| text.split_at(p));
|
||||
let deserialized = (|| {
|
||||
let $numname = num.parse::<$numty>().ok()?;
|
||||
let $unitname = unit.trim();
|
||||
$body
|
||||
})();
|
||||
if deserialized.is_some() {
|
||||
Ok(deserialized)
|
||||
} else {
|
||||
Err(de::Error::custom(
|
||||
"Invalid value, please refer to the documentation",
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
generate_deserializer!(deserialize_duration(num: u64, unit: &str) -> Option<Duration> {
|
||||
match unit {
|
||||
"s" => Some(Duration::from_secs(num)),
|
||||
"m" => Some(Duration::from_secs(num * 60)),
|
||||
"h" => Some(Duration::from_secs(num * 60 * 60)),
|
||||
"d" => Some(Duration::from_secs(num * 60 * 60 * 24)),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
generate_deserializer!(deserialize_si_prefix(num: u64, unit: &str) -> Option<u64> {
|
||||
match unit {
|
||||
"" => Some(num),
|
||||
"K" => num.checked_mul(1_000),
|
||||
"M" => num.checked_mul(1_000_000),
|
||||
"G" => num.checked_mul(1_000_000_000),
|
||||
"T" => num.checked_mul(1_000_000_000_000),
|
||||
"P" => num.checked_mul(1_000_000_000_000_000),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
generate_deserializer!(deserialize_disk_space(num: u64, unit: &str) -> Option<u64> {
|
||||
match unit {
|
||||
"" => Some(num),
|
||||
"K" => num.checked_mul(1_000),
|
||||
"Ki" => num.checked_mul(1u64 << 10),
|
||||
"M" => num.checked_mul(1_000_000),
|
||||
"Mi" => num.checked_mul(1u64 << 20),
|
||||
"G" => num.checked_mul(1_000_000_000),
|
||||
"Gi" => num.checked_mul(1u64 << 30),
|
||||
"T" => num.checked_mul(1_000_000_000_000),
|
||||
"Ti" => num.checked_mul(1u64 << 40),
|
||||
"P" => num.checked_mul(1_000_000_000_000_000),
|
||||
"Pi" => num.checked_mul(1u64 << 50),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
generate_deserializer!(deserialize_percent(num: u8, unit: &str) -> Option<u8> {
|
||||
match unit {
|
||||
"%" => Some(num),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
static CACHE_IMPROPER_CONFIG_ERROR_MSG: &str =
|
||||
"Cache system should be enabled and all settings must be validated or defaulted";
|
||||
|
||||
macro_rules! generate_setting_getter {
|
||||
($setting:ident: $setting_type:ty) => {
|
||||
/// Returns `$setting`.
|
||||
///
|
||||
/// Panics if the cache is disabled.
|
||||
pub fn $setting(&self) -> $setting_type {
|
||||
self
|
||||
.$setting
|
||||
.expect(CACHE_IMPROPER_CONFIG_ERROR_MSG)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl CacheConfig {
|
||||
generate_setting_getter!(worker_event_queue_size: u64);
|
||||
generate_setting_getter!(baseline_compression_level: i32);
|
||||
generate_setting_getter!(optimized_compression_level: i32);
|
||||
generate_setting_getter!(optimized_compression_usage_counter_threshold: u64);
|
||||
generate_setting_getter!(cleanup_interval: Duration);
|
||||
generate_setting_getter!(optimizing_compression_task_timeout: Duration);
|
||||
generate_setting_getter!(allowed_clock_drift_for_files_from_future: Duration);
|
||||
generate_setting_getter!(file_count_soft_limit: u64);
|
||||
generate_setting_getter!(files_total_size_soft_limit: u64);
|
||||
generate_setting_getter!(file_count_limit_percent_if_deleting: u8);
|
||||
generate_setting_getter!(files_total_size_limit_percent_if_deleting: u8);
|
||||
|
||||
/// Returns true if and only if the cache is enabled.
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
/// Returns path to the cache directory.
|
||||
///
|
||||
/// Panics if the cache is disabled.
|
||||
pub fn directory(&self) -> &PathBuf {
|
||||
self.directory
|
||||
.as_ref()
|
||||
.expect(CACHE_IMPROPER_CONFIG_ERROR_MSG)
|
||||
}
|
||||
|
||||
pub fn new_cache_disabled() -> Self {
|
||||
Self {
|
||||
errors: Vec::new(),
|
||||
enabled: false,
|
||||
directory: None,
|
||||
worker_event_queue_size: None,
|
||||
baseline_compression_level: None,
|
||||
optimized_compression_level: None,
|
||||
optimized_compression_usage_counter_threshold: None,
|
||||
cleanup_interval: None,
|
||||
optimizing_compression_task_timeout: None,
|
||||
allowed_clock_drift_for_files_from_future: None,
|
||||
file_count_soft_limit: None,
|
||||
files_total_size_soft_limit: None,
|
||||
file_count_limit_percent_if_deleting: None,
|
||||
files_total_size_limit_percent_if_deleting: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_cache_enabled_template() -> Self {
|
||||
let mut conf = Self::new_cache_disabled();
|
||||
conf.enabled = true;
|
||||
conf
|
||||
}
|
||||
|
||||
fn new_cache_with_errors(errors: Vec<String>) -> Self {
|
||||
let mut conf = Self::new_cache_disabled();
|
||||
conf.errors = errors;
|
||||
conf
|
||||
}
|
||||
|
||||
pub fn from_file<P: AsRef<Path>>(enabled: bool, config_file: Option<P>) -> Self {
|
||||
if !enabled {
|
||||
return Self::new_cache_disabled();
|
||||
}
|
||||
|
||||
let mut config = match Self::load_and_parse_file(config_file) {
|
||||
Ok(data) => data,
|
||||
Err(err) => return Self::new_cache_with_errors(vec![err]),
|
||||
};
|
||||
|
||||
// validate values and fill in defaults
|
||||
config.validate_directory_or_default();
|
||||
config.validate_worker_event_queue_size_or_default();
|
||||
config.validate_baseline_compression_level_or_default();
|
||||
config.validate_optimized_compression_level_or_default();
|
||||
config.validate_optimized_compression_usage_counter_threshold_or_default();
|
||||
config.validate_cleanup_interval_or_default();
|
||||
config.validate_optimizing_compression_task_timeout_or_default();
|
||||
config.validate_allowed_clock_drift_for_files_from_future_or_default();
|
||||
config.validate_file_count_soft_limit_or_default();
|
||||
config.validate_files_total_size_soft_limit_or_default();
|
||||
config.validate_file_count_limit_percent_if_deleting_or_default();
|
||||
config.validate_files_total_size_limit_percent_if_deleting_or_default();
|
||||
|
||||
config.disable_if_any_error();
|
||||
config
|
||||
}
|
||||
|
||||
fn load_and_parse_file<P: AsRef<Path>>(config_file: Option<P>) -> Result<Self, String> {
|
||||
// get config file path
|
||||
let (config_file, user_custom_file) = config_file.as_ref().map_or_else(
|
||||
|| DEFAULT_CONFIG_PATH.as_ref().map(|p| (p.as_ref(), false)),
|
||||
|p| Ok((p.as_ref(), true)),
|
||||
)?;
|
||||
|
||||
// read config, or use default one
|
||||
let entity_exists = config_file.exists();
|
||||
match (entity_exists, user_custom_file) {
|
||||
(false, false) => Ok(Self::new_cache_enabled_template()),
|
||||
_ => match fs::read(&config_file) {
|
||||
Ok(bytes) => match toml::from_slice::<Config>(&bytes[..]) {
|
||||
Ok(config) => Ok(config.cache),
|
||||
Err(err) => Err(format!(
|
||||
"Failed to parse config file, path: {}, error: {}",
|
||||
config_file.display(),
|
||||
err
|
||||
)),
|
||||
},
|
||||
Err(err) => Err(format!(
|
||||
"Failed to read config file, path: {}, error: {}",
|
||||
config_file.display(),
|
||||
err
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_directory_or_default(&mut self) {
|
||||
if self.directory.is_none() {
|
||||
match &*PROJECT_DIRS {
|
||||
Some(proj_dirs) => self.directory = Some(proj_dirs.cache_dir().to_path_buf()),
|
||||
None => {
|
||||
self.errors.push(
|
||||
"Cache directory not specified and failed to get the default".to_string(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On Windows, if we want long paths, we need '\\?\' prefix, but it doesn't work
|
||||
// with relative paths. One way to get absolute path (the only one?) is to use
|
||||
// fs::canonicalize, but it requires that given path exists. The extra advantage
|
||||
// of this method is fact that the method prepends '\\?\' on Windows.
|
||||
let cache_dir = self.directory.as_ref().unwrap();
|
||||
|
||||
if !cache_dir.is_absolute() {
|
||||
self.errors.push(format!(
|
||||
"Cache directory path has to be absolute, path: {}",
|
||||
cache_dir.display(),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
match fs::create_dir_all(cache_dir) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
self.errors.push(format!(
|
||||
"Failed to create the cache directory, path: {}, error: {}",
|
||||
cache_dir.display(),
|
||||
err
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match fs::canonicalize(cache_dir) {
|
||||
Ok(p) => self.directory = Some(p),
|
||||
Err(err) => {
|
||||
self.errors.push(format!(
|
||||
"Failed to canonicalize the cache directory, path: {}, error: {}",
|
||||
cache_dir.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_worker_event_queue_size_or_default(&mut self) {
|
||||
if self.worker_event_queue_size.is_none() {
|
||||
self.worker_event_queue_size = Some(DEFAULT_WORKER_EVENT_QUEUE_SIZE);
|
||||
}
|
||||
|
||||
if self.worker_event_queue_size.unwrap() < WORKER_EVENT_QUEUE_SIZE_WARNING_TRESHOLD {
|
||||
warn!("Detected small worker event queue size. Some messages might be lost.");
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_baseline_compression_level_or_default(&mut self) {
|
||||
if self.baseline_compression_level.is_none() {
|
||||
self.baseline_compression_level = Some(DEFAULT_BASELINE_COMPRESSION_LEVEL);
|
||||
}
|
||||
|
||||
if !ZSTD_COMPRESSION_LEVELS.contains(&self.baseline_compression_level.unwrap()) {
|
||||
self.errors.push(format!(
|
||||
"Invalid baseline compression level: {} not in {:#?}",
|
||||
self.baseline_compression_level.unwrap(),
|
||||
ZSTD_COMPRESSION_LEVELS
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// assumption: baseline compression level has been verified
|
||||
fn validate_optimized_compression_level_or_default(&mut self) {
|
||||
if self.optimized_compression_level.is_none() {
|
||||
self.optimized_compression_level = Some(DEFAULT_OPTIMIZED_COMPRESSION_LEVEL);
|
||||
}
|
||||
|
||||
let opt_lvl = self.optimized_compression_level.unwrap();
|
||||
let base_lvl = self.baseline_compression_level.unwrap();
|
||||
|
||||
if !ZSTD_COMPRESSION_LEVELS.contains(&opt_lvl) {
|
||||
self.errors.push(format!(
|
||||
"Invalid optimized compression level: {} not in {:#?}",
|
||||
opt_lvl, ZSTD_COMPRESSION_LEVELS
|
||||
));
|
||||
}
|
||||
|
||||
if opt_lvl < base_lvl {
|
||||
self.errors.push(format!(
|
||||
"Invalid optimized compression level is lower than baseline: {} < {}",
|
||||
opt_lvl, base_lvl
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_optimized_compression_usage_counter_threshold_or_default(&mut self) {
|
||||
if self.optimized_compression_usage_counter_threshold.is_none() {
|
||||
self.optimized_compression_usage_counter_threshold =
|
||||
Some(DEFAULT_OPTIMIZED_COMPRESSION_USAGE_COUNTER_THRESHOLD);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_cleanup_interval_or_default(&mut self) {
|
||||
if self.cleanup_interval.is_none() {
|
||||
self.cleanup_interval = Some(DEFAULT_CLEANUP_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_optimizing_compression_task_timeout_or_default(&mut self) {
|
||||
if self.optimizing_compression_task_timeout.is_none() {
|
||||
self.optimizing_compression_task_timeout =
|
||||
Some(DEFAULT_OPTIMIZING_COMPRESSION_TASK_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_allowed_clock_drift_for_files_from_future_or_default(&mut self) {
|
||||
if self.allowed_clock_drift_for_files_from_future.is_none() {
|
||||
self.allowed_clock_drift_for_files_from_future =
|
||||
Some(DEFAULT_ALLOWED_CLOCK_DRIFT_FOR_FILES_FROM_FUTURE);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_file_count_soft_limit_or_default(&mut self) {
|
||||
if self.file_count_soft_limit.is_none() {
|
||||
self.file_count_soft_limit = Some(DEFAULT_FILE_COUNT_SOFT_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_files_total_size_soft_limit_or_default(&mut self) {
|
||||
if self.files_total_size_soft_limit.is_none() {
|
||||
self.files_total_size_soft_limit = Some(DEFAULT_FILES_TOTAL_SIZE_SOFT_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_file_count_limit_percent_if_deleting_or_default(&mut self) {
|
||||
if self.file_count_limit_percent_if_deleting.is_none() {
|
||||
self.file_count_limit_percent_if_deleting =
|
||||
Some(DEFAULT_FILE_COUNT_LIMIT_PERCENT_IF_DELETING);
|
||||
}
|
||||
|
||||
let percent = self.file_count_limit_percent_if_deleting.unwrap();
|
||||
if percent > 100 {
|
||||
self.errors.push(format!(
|
||||
"Invalid files count limit percent if deleting: {} not in range 0-100%",
|
||||
percent
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_files_total_size_limit_percent_if_deleting_or_default(&mut self) {
|
||||
if self.files_total_size_limit_percent_if_deleting.is_none() {
|
||||
self.files_total_size_limit_percent_if_deleting =
|
||||
Some(DEFAULT_FILES_TOTAL_SIZE_LIMIT_PERCENT_IF_DELETING);
|
||||
}
|
||||
|
||||
let percent = self.files_total_size_limit_percent_if_deleting.unwrap();
|
||||
if percent > 100 {
|
||||
self.errors.push(format!(
|
||||
"Invalid files total size limit percent if deleting: {} not in range 0-100%",
|
||||
percent
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn disable_if_any_error(&mut self) {
|
||||
if !self.errors.is_empty() {
|
||||
let mut conf = Self::new_cache_disabled();
|
||||
mem::swap(self, &mut conf);
|
||||
mem::swap(&mut self.errors, &mut conf.errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
pub mod tests;
|
||||
581
crates/environ/src/cache/config/tests.rs
vendored
Normal file
581
crates/environ/src/cache/config/tests.rs
vendored
Normal file
@@ -0,0 +1,581 @@
|
||||
use super::CacheConfig;
|
||||
use core::time::Duration;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::{self, TempDir};
|
||||
|
||||
// note: config loading during validation creates cache directory to canonicalize its path,
|
||||
// that's why these function and macro always use custom cache directory
|
||||
// note: tempdir removes directory when being dropped, so we need to return it to the caller,
|
||||
// so the paths are valid
|
||||
pub fn test_prolog() -> (TempDir, PathBuf, PathBuf) {
|
||||
let _ = pretty_env_logger::try_init();
|
||||
let temp_dir = tempfile::tempdir().expect("Can't create temporary directory");
|
||||
let cache_dir = temp_dir.path().join("cache-dir");
|
||||
let config_path = temp_dir.path().join("cache-config.toml");
|
||||
(temp_dir, cache_dir, config_path)
|
||||
}
|
||||
|
||||
macro_rules! load_config {
|
||||
($config_path:ident, $content_fmt:expr, $cache_dir:ident) => {{
|
||||
let config_path = &$config_path;
|
||||
let content = format!(
|
||||
$content_fmt,
|
||||
cache_dir = toml::to_string_pretty(&format!("{}", $cache_dir.display())).unwrap()
|
||||
);
|
||||
fs::write(config_path, content).expect("Failed to write test config file");
|
||||
CacheConfig::from_file(true, Some(config_path))
|
||||
}};
|
||||
}
|
||||
|
||||
// test without macros to test being disabled
|
||||
#[test]
|
||||
fn test_disabled() {
|
||||
let dir = tempfile::tempdir().expect("Can't create temporary directory");
|
||||
let config_path = dir.path().join("cache-config.toml");
|
||||
let config_content = "[cache]\n\
|
||||
enabled = true\n";
|
||||
fs::write(&config_path, config_content).expect("Failed to write test config file");
|
||||
let conf = CacheConfig::from_file(false, Some(&config_path));
|
||||
assert!(!conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
|
||||
let config_content = "[cache]\n\
|
||||
enabled = false\n";
|
||||
fs::write(&config_path, config_content).expect("Failed to write test config file");
|
||||
let conf = CacheConfig::from_file(true, Some(&config_path));
|
||||
assert!(!conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unrecognized_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"unrecognized-setting = 42\n\
|
||||
[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
unrecognized-setting = 42",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level = 20\n\
|
||||
optimized-compression-usage-counter-threshold = '256'\n\
|
||||
cleanup-interval = '1h'\n\
|
||||
optimizing-compression-task-timeout = '30m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1d'\n\
|
||||
file-count-soft-limit = '65536'\n\
|
||||
files-total-size-soft-limit = '512Mi'\n\
|
||||
file-count-limit-percent-if-deleting = '70%'\n\
|
||||
files-total-size-limit-percent-if-deleting = '70%'",
|
||||
cd
|
||||
);
|
||||
check_conf(&conf, &cd);
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
// added some white spaces
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = ' 16\t'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level =\t 20\n\
|
||||
optimized-compression-usage-counter-threshold = '256'\n\
|
||||
cleanup-interval = ' 1h'\n\
|
||||
optimizing-compression-task-timeout = '30 m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1\td'\n\
|
||||
file-count-soft-limit = '\t \t65536\t'\n\
|
||||
files-total-size-soft-limit = '512\t\t Mi '\n\
|
||||
file-count-limit-percent-if-deleting = '70\t%'\n\
|
||||
files-total-size-limit-percent-if-deleting = ' 70 %'",
|
||||
cd
|
||||
);
|
||||
check_conf(&conf, &cd);
|
||||
|
||||
fn check_conf(conf: &CacheConfig, cd: &PathBuf) {
|
||||
eprintln!("errors: {:#?}", conf.errors);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(
|
||||
conf.directory(),
|
||||
&fs::canonicalize(cd).expect("canonicalize failed")
|
||||
);
|
||||
assert_eq!(conf.worker_event_queue_size(), 0x10);
|
||||
assert_eq!(conf.baseline_compression_level(), 3);
|
||||
assert_eq!(conf.optimized_compression_level(), 20);
|
||||
assert_eq!(conf.optimized_compression_usage_counter_threshold(), 0x100);
|
||||
assert_eq!(conf.cleanup_interval(), Duration::from_secs(60 * 60));
|
||||
assert_eq!(
|
||||
conf.optimizing_compression_task_timeout(),
|
||||
Duration::from_secs(30 * 60)
|
||||
);
|
||||
assert_eq!(
|
||||
conf.allowed_clock_drift_for_files_from_future(),
|
||||
Duration::from_secs(60 * 60 * 24)
|
||||
);
|
||||
assert_eq!(conf.file_count_soft_limit(), 0x10_000);
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 512 * (1u64 << 20));
|
||||
assert_eq!(conf.file_count_limit_percent_if_deleting(), 70);
|
||||
assert_eq!(conf.files_total_size_limit_percent_if_deleting(), 70);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compression_level_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
baseline-compression-level = 1\n\
|
||||
optimized-compression-level = 21",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.baseline_compression_level(), 1);
|
||||
assert_eq!(conf.optimized_compression_level(), 21);
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
baseline-compression-level = -1\n\
|
||||
optimized-compression-level = 21",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
baseline-compression-level = 15\n\
|
||||
optimized-compression-level = 10",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_si_prefix_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '42'\n\
|
||||
optimized-compression-usage-counter-threshold = '4K'\n\
|
||||
file-count-soft-limit = '3M'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.worker_event_queue_size(), 42);
|
||||
assert_eq!(conf.optimized_compression_usage_counter_threshold(), 4_000);
|
||||
assert_eq!(conf.file_count_soft_limit(), 3_000_000);
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '2G'\n\
|
||||
optimized-compression-usage-counter-threshold = '4444T'\n\
|
||||
file-count-soft-limit = '1P'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.worker_event_queue_size(), 2_000_000_000);
|
||||
assert_eq!(
|
||||
conf.optimized_compression_usage_counter_threshold(),
|
||||
4_444_000_000_000_000
|
||||
);
|
||||
assert_eq!(conf.file_count_soft_limit(), 1_000_000_000_000_000);
|
||||
|
||||
// different errors
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '2g'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
file-count-soft-limit = 1",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
file-count-soft-limit = '-31337'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
file-count-soft-limit = '3.14M'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disk_space_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '76'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 76);
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '42 Mi'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 42 * (1u64 << 20));
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '2 Gi'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 2 * (1u64 << 30));
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '31337 Ti'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 31337 * (1u64 << 40));
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '7 Pi'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 7 * (1u64 << 50));
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '7M'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 7_000_000);
|
||||
|
||||
// different errors
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '7 mi'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = 1",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '-31337'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '3.14Ki'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
cleanup-interval = '100s'\n\
|
||||
optimizing-compression-task-timeout = '3m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '4h'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.cleanup_interval(), Duration::from_secs(100));
|
||||
assert_eq!(
|
||||
conf.optimizing_compression_task_timeout(),
|
||||
Duration::from_secs(3 * 60)
|
||||
);
|
||||
assert_eq!(
|
||||
conf.allowed_clock_drift_for_files_from_future(),
|
||||
Duration::from_secs(4 * 60 * 60)
|
||||
);
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
cleanup-interval = '2d'\n\
|
||||
optimizing-compression-task-timeout = '333 m'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(
|
||||
conf.cleanup_interval(),
|
||||
Duration::from_secs(2 * 24 * 60 * 60)
|
||||
);
|
||||
assert_eq!(
|
||||
conf.optimizing_compression_task_timeout(),
|
||||
Duration::from_secs(333 * 60)
|
||||
);
|
||||
|
||||
// different errors
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = '333'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = 333",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = '10 M'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = '10 min'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = '-10s'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = '1.5m'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_percent_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
file-count-limit-percent-if-deleting = '62%'\n\
|
||||
files-total-size-limit-percent-if-deleting = '23 %'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.file_count_limit_percent_if_deleting(), 62);
|
||||
assert_eq!(conf.files_total_size_limit_percent_if_deleting(), 23);
|
||||
|
||||
// different errors
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-limit-percent-if-deleting = '23'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-limit-percent-if-deleting = '22.5%'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-limit-percent-if-deleting = '0.5'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-limit-percent-if-deleting = '-1%'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-limit-percent-if-deleting = '101%'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
354
crates/environ/src/cache/tests.rs
vendored
Normal file
354
crates/environ/src/cache/tests.rs
vendored
Normal file
@@ -0,0 +1,354 @@
|
||||
use super::config::tests::test_prolog;
|
||||
use super::*;
|
||||
use crate::address_map::{FunctionAddressMap, InstructionAddressMap};
|
||||
use crate::compilation::{CompiledFunction, Relocation, RelocationTarget, TrapInformation};
|
||||
use crate::module::{MemoryPlan, MemoryStyle, Module};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec::Vec;
|
||||
use core::cmp::min;
|
||||
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::fs;
|
||||
use std::str::FromStr;
|
||||
use target_lexicon::triple;
|
||||
|
||||
// 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.
|
||||
// We test init() in exactly one test, rest of the tests doesn't rely on it.
|
||||
|
||||
#[test]
|
||||
fn test_cache_init() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let baseline_compression_level = 4;
|
||||
let config_content = format!(
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {}\n\
|
||||
baseline-compression-level = {}\n",
|
||||
toml::to_string_pretty(&format!("{}", cache_dir.display())).unwrap(),
|
||||
baseline_compression_level,
|
||||
);
|
||||
fs::write(&config_path, config_content).expect("Failed to write test config file");
|
||||
|
||||
let errors = init(true, Some(&config_path), None);
|
||||
assert!(errors.is_empty());
|
||||
|
||||
// test if we can use config
|
||||
let cache_config = cache_config();
|
||||
assert!(cache_config.enabled());
|
||||
// assumption: config init creates cache directory and returns canonicalized path
|
||||
assert_eq!(
|
||||
*cache_config.directory(),
|
||||
fs::canonicalize(cache_dir).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
cache_config.baseline_compression_level(),
|
||||
baseline_compression_level
|
||||
);
|
||||
|
||||
// test if we can use worker
|
||||
let worker = worker();
|
||||
worker.on_cache_update_async(config_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_read_cache() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
baseline-compression-level = 3\n",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
// assumption: config load creates cache directory and returns canonicalized path
|
||||
assert_eq!(
|
||||
*cache_config.directory(),
|
||||
fs::canonicalize(cache_dir).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-unknown-unknown");
|
||||
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::from_inner(ModuleCacheEntryInner::new(
|
||||
&module1,
|
||||
&function_body_inputs1,
|
||||
&*isa1,
|
||||
compiler1,
|
||||
false,
|
||||
&cache_config,
|
||||
&worker,
|
||||
));
|
||||
assert!(entry1.0.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::from_inner(ModuleCacheEntryInner::new(
|
||||
&module2,
|
||||
&function_body_inputs1,
|
||||
&*isa1,
|
||||
compiler1,
|
||||
false,
|
||||
&cache_config,
|
||||
&worker,
|
||||
));
|
||||
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::from_inner(ModuleCacheEntryInner::new(
|
||||
&module1,
|
||||
&function_body_inputs2,
|
||||
&*isa1,
|
||||
compiler1,
|
||||
false,
|
||||
&cache_config,
|
||||
&worker,
|
||||
));
|
||||
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::from_inner(ModuleCacheEntryInner::new(
|
||||
&module1,
|
||||
&function_body_inputs1,
|
||||
&*isa2,
|
||||
compiler1,
|
||||
false,
|
||||
&cache_config,
|
||||
&worker,
|
||||
));
|
||||
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::from_inner(ModuleCacheEntryInner::new(
|
||||
&module1,
|
||||
&function_body_inputs1,
|
||||
&*isa1,
|
||||
compiler2,
|
||||
false,
|
||||
&cache_config,
|
||||
&worker,
|
||||
));
|
||||
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 {
|
||||
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
|
||||
}
|
||||
});
|
||||
CompiledFunction {
|
||||
body: (0..(i * 3 / 2)).collect(),
|
||||
jt_offsets: sm,
|
||||
unwind_info: (0..(i * 3 / 2)).collect(),
|
||||
}
|
||||
})
|
||||
.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();
|
||||
|
||||
let traps = (0..rng.gen_range(0, 0xd))
|
||||
.map(|i| {
|
||||
((i..i + rng.gen_range(0, 4))
|
||||
.map(|_| TrapInformation {
|
||||
code_offset: rng.gen(),
|
||||
source_loc: ir::SourceLoc::new(rng.gen()),
|
||||
trap_code: ir::TrapCode::StackOverflow,
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
.collect();
|
||||
|
||||
ModuleCacheData::from_tuple((
|
||||
Compilation::new(funcs),
|
||||
relocs,
|
||||
trans,
|
||||
value_ranges,
|
||||
stack_slots,
|
||||
traps,
|
||||
))
|
||||
}
|
||||
912
crates/environ/src/cache/worker.rs
vendored
Normal file
912
crates/environ/src/cache/worker.rs
vendored
Normal file
@@ -0,0 +1,912 @@
|
||||
//! Background worker that watches over the cache.
|
||||
//!
|
||||
//! It cleans up old cache, updates statistics and optimizes the cache.
|
||||
//! We allow losing some messages (it doesn't hurt) and some races,
|
||||
//! but we guarantee eventual consistency and fault tolerancy.
|
||||
//! Background tasks can be CPU intensive, but the worker thread has low priority.
|
||||
|
||||
use super::{cache_config, fs_write_atomic, CacheConfig};
|
||||
use alloc::vec::Vec;
|
||||
use core::cmp;
|
||||
use core::time::Duration;
|
||||
use log::{debug, info, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use spin::Once;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
||||
#[cfg(test)]
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::thread;
|
||||
#[cfg(not(test))]
|
||||
use std::time::SystemTime;
|
||||
#[cfg(test)]
|
||||
use tests::system_time_stub::SystemTimeStub as SystemTime;
|
||||
|
||||
pub(super) struct Worker {
|
||||
sender: SyncSender<CacheEvent>,
|
||||
#[cfg(test)]
|
||||
stats: Arc<(Mutex<WorkerStats>, Condvar)>,
|
||||
}
|
||||
|
||||
struct WorkerThread {
|
||||
receiver: Receiver<CacheEvent>,
|
||||
cache_config: CacheConfig,
|
||||
#[cfg(test)]
|
||||
stats: Arc<(Mutex<WorkerStats>, Condvar)>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(Default)]
|
||||
struct WorkerStats {
|
||||
dropped: u32,
|
||||
sent: u32,
|
||||
handled: u32,
|
||||
}
|
||||
|
||||
static WORKER: Once<Worker> = Once::new();
|
||||
static INIT_CALLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub(super) fn worker() -> &'static Worker {
|
||||
WORKER
|
||||
.r#try()
|
||||
.expect("Cache worker must be initialized before usage")
|
||||
}
|
||||
|
||||
pub(super) fn init(init_file_per_thread_logger: Option<&'static str>) {
|
||||
INIT_CALLED
|
||||
.compare_exchange(
|
||||
false,
|
||||
true,
|
||||
atomic::Ordering::SeqCst,
|
||||
atomic::Ordering::SeqCst,
|
||||
)
|
||||
.expect("Cache worker init must be called at most once");
|
||||
|
||||
let worker = Worker::start_new(cache_config(), init_file_per_thread_logger);
|
||||
WORKER.call_once(|| worker);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum CacheEvent {
|
||||
OnCacheGet(PathBuf),
|
||||
OnCacheUpdate(PathBuf),
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub(super) fn start_new(
|
||||
cache_config: &CacheConfig,
|
||||
init_file_per_thread_logger: Option<&'static str>,
|
||||
) -> Self {
|
||||
let queue_size = match cache_config.worker_event_queue_size() {
|
||||
num if num <= usize::max_value() as u64 => num as usize,
|
||||
_ => usize::max_value(),
|
||||
};
|
||||
let (tx, rx) = sync_channel(queue_size);
|
||||
|
||||
#[cfg(test)]
|
||||
let stats = Arc::new((Mutex::new(WorkerStats::default()), Condvar::new()));
|
||||
|
||||
let worker_thread = WorkerThread {
|
||||
receiver: rx,
|
||||
cache_config: cache_config.clone(),
|
||||
#[cfg(test)]
|
||||
stats: stats.clone(),
|
||||
};
|
||||
|
||||
// when self is dropped, sender will be dropped, what will cause the channel
|
||||
// to hang, and the worker thread to exit -- it happens in the tests
|
||||
// non-tests binary has only a static worker, so Rust doesn't drop it
|
||||
thread::spawn(move || worker_thread.run(init_file_per_thread_logger));
|
||||
|
||||
Self {
|
||||
sender: tx,
|
||||
#[cfg(test)]
|
||||
stats,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_cache_get_async(&self, path: impl AsRef<Path>) {
|
||||
let event = CacheEvent::OnCacheGet(path.as_ref().to_path_buf());
|
||||
self.send_cache_event(event);
|
||||
}
|
||||
|
||||
pub(super) fn on_cache_update_async(&self, path: impl AsRef<Path>) {
|
||||
let event = CacheEvent::OnCacheUpdate(path.as_ref().to_path_buf());
|
||||
self.send_cache_event(event);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn send_cache_event(&self, event: CacheEvent) {
|
||||
#[cfg(test)]
|
||||
let mut stats = self
|
||||
.stats
|
||||
.0
|
||||
.lock()
|
||||
.expect("Failed to acquire worker stats lock");
|
||||
match self.sender.try_send(event.clone()) {
|
||||
Ok(()) => {
|
||||
#[cfg(test)]
|
||||
let _ = stats.sent += 1;
|
||||
}
|
||||
Err(err) => {
|
||||
info!(
|
||||
"Failed to send asynchronously message to worker thread, \
|
||||
event: {:?}, error: {}",
|
||||
event, err
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
let _ = stats.dropped += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn events_dropped(&self) -> u32 {
|
||||
let stats = self
|
||||
.stats
|
||||
.0
|
||||
.lock()
|
||||
.expect("Failed to acquire worker stats lock");
|
||||
stats.dropped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn wait_for_all_events_handled(&self) {
|
||||
let (stats, condvar) = &*self.stats;
|
||||
let mut stats = stats.lock().expect("Failed to acquire worker stats lock");
|
||||
while stats.handled != stats.sent {
|
||||
stats = condvar
|
||||
.wait(stats)
|
||||
.expect("Failed to reacquire worker stats lock");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ModuleCacheStatistics {
|
||||
pub usages: u64,
|
||||
#[serde(rename = "optimized-compression")]
|
||||
pub compression_level: i32,
|
||||
}
|
||||
|
||||
impl ModuleCacheStatistics {
|
||||
fn default(cache_config: &CacheConfig) -> Self {
|
||||
Self {
|
||||
usages: 0,
|
||||
compression_level: cache_config.baseline_compression_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CacheEntry {
|
||||
Recognized {
|
||||
path: PathBuf,
|
||||
mtime: SystemTime,
|
||||
size: u64,
|
||||
},
|
||||
Unrecognized {
|
||||
path: PathBuf,
|
||||
is_dir: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl WorkerThread {
|
||||
fn run(self, init_file_per_thread_logger: Option<&'static str>) {
|
||||
#[cfg(not(test))] // We want to test the worker without relying on init() being called
|
||||
assert!(INIT_CALLED.load(atomic::Ordering::SeqCst));
|
||||
|
||||
if let Some(prefix) = init_file_per_thread_logger {
|
||||
file_per_thread_logger::initialize(prefix);
|
||||
}
|
||||
|
||||
debug!("Cache worker thread started.");
|
||||
|
||||
Self::lower_thread_priority();
|
||||
|
||||
#[cfg(test)]
|
||||
let (stats, condvar) = &*self.stats;
|
||||
|
||||
for event in self.receiver.iter() {
|
||||
match event {
|
||||
CacheEvent::OnCacheGet(path) => self.handle_on_cache_get(path),
|
||||
CacheEvent::OnCacheUpdate(path) => self.handle_on_cache_update(path),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
{
|
||||
let mut stats = stats.lock().expect("Failed to acquire worker stats lock");
|
||||
stats.handled += 1;
|
||||
condvar.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
// The receiver can stop iteration iff the channel has hung up.
|
||||
// The channel will hung when sender is dropped. It only happens in tests.
|
||||
// In non-test case we have static worker and Rust doesn't drop static variables.
|
||||
#[cfg(not(test))]
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn lower_thread_priority() {
|
||||
use core::convert::TryInto;
|
||||
use winapi::um::processthreadsapi::{GetCurrentThread, SetThreadPriority};
|
||||
use winapi::um::winbase::THREAD_MODE_BACKGROUND_BEGIN;
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadpriority
|
||||
// https://docs.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities
|
||||
|
||||
if unsafe {
|
||||
SetThreadPriority(
|
||||
GetCurrentThread(),
|
||||
THREAD_MODE_BACKGROUND_BEGIN.try_into().unwrap(),
|
||||
)
|
||||
} == 0
|
||||
{
|
||||
warn!(
|
||||
"Failed to lower worker thread priority. It might affect application performance."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn lower_thread_priority() {
|
||||
// http://man7.org/linux/man-pages/man7/sched.7.html
|
||||
|
||||
const NICE_DELTA_FOR_BACKGROUND_TASKS: i32 = 3;
|
||||
|
||||
errno::set_errno(errno::Errno(0));
|
||||
let current_nice = unsafe { libc::nice(NICE_DELTA_FOR_BACKGROUND_TASKS) };
|
||||
let errno_val = errno::errno().0;
|
||||
|
||||
if errno_val != 0 {
|
||||
warn!("Failed to lower worker thread priority. It might affect application performance. errno: {}", errno_val);
|
||||
} else {
|
||||
debug!("New nice value of worker thread: {}", current_nice);
|
||||
}
|
||||
}
|
||||
|
||||
/// Increases the usage counter and recompresses the file
|
||||
/// if the usage counter reached configurable treshold.
|
||||
fn handle_on_cache_get(&self, path: PathBuf) {
|
||||
trace!("handle_on_cache_get() for path: {}", path.display());
|
||||
|
||||
// construct .stats file path
|
||||
let filename = path.file_name().unwrap().to_str().unwrap();
|
||||
let stats_path = path.with_file_name(format!("{}.stats", filename));
|
||||
|
||||
// load .stats file (default if none or error)
|
||||
let mut stats = read_stats_file(stats_path.as_ref())
|
||||
.unwrap_or_else(|| ModuleCacheStatistics::default(&self.cache_config));
|
||||
|
||||
// step 1: update the usage counter & write to the disk
|
||||
// it's racy, but it's fine (the counter will be just smaller,
|
||||
// sometimes will retrigger recompression)
|
||||
stats.usages += 1;
|
||||
if !write_stats_file(stats_path.as_ref(), &stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
// step 2: recompress if there's a need
|
||||
let opt_compr_lvl = self.cache_config.optimized_compression_level();
|
||||
if stats.compression_level >= opt_compr_lvl
|
||||
|| stats.usages
|
||||
< self
|
||||
.cache_config
|
||||
.optimized_compression_usage_counter_threshold()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let lock_path = if let Some(p) = acquire_task_fs_lock(
|
||||
path.as_ref(),
|
||||
self.cache_config.optimizing_compression_task_timeout(),
|
||||
self.cache_config
|
||||
.allowed_clock_drift_for_files_from_future(),
|
||||
) {
|
||||
p
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
trace!("Trying to recompress file: {}", path.display());
|
||||
|
||||
// recompress, write to other file, rename (it's atomic file content exchange)
|
||||
// and update the stats file
|
||||
fs::read(&path)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to read old cache file, path: {}, err: {}",
|
||||
path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
.and_then(|compressed_cache_bytes| {
|
||||
zstd::decode_all(&compressed_cache_bytes[..])
|
||||
.map_err(|err| warn!("Failed to decompress cached code: {}", err))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|cache_bytes| {
|
||||
zstd::encode_all(
|
||||
&cache_bytes[..],
|
||||
opt_compr_lvl,
|
||||
)
|
||||
.map_err(|err| warn!("Failed to compress cached code: {}", err))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|recompressed_cache_bytes| {
|
||||
fs::write(&lock_path, &recompressed_cache_bytes)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to write recompressed cache, path: {}, err: {}",
|
||||
lock_path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.and_then(|()| {
|
||||
fs::rename(&lock_path, &path)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to rename recompressed cache, path from: {}, path to: {}, err: {}",
|
||||
lock_path.display(),
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
if let Err(err) = fs::remove_file(&lock_path) {
|
||||
warn!(
|
||||
"Failed to clean up (remove) recompressed cache, path {}, err: {}",
|
||||
lock_path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.map(|()| {
|
||||
// update stats file (reload it! recompression can take some time)
|
||||
if let Some(mut new_stats) = read_stats_file(stats_path.as_ref()) {
|
||||
if new_stats.compression_level >= opt_compr_lvl {
|
||||
// Rare race:
|
||||
// two instances with different opt_compr_lvl: we don't know in which order they updated
|
||||
// the cache file and the stats file (they are not updated together atomically)
|
||||
// Possible solution is to use directories per cache entry, but it complicates the system
|
||||
// and is not worth it.
|
||||
debug!("DETECTED task did more than once (or race with new file): recompression of {}. \
|
||||
Note: if optimized compression level setting has changed in the meantine, \
|
||||
the stats file might contain inconsistent compression level due to race.", path.display());
|
||||
}
|
||||
else {
|
||||
new_stats.compression_level = opt_compr_lvl;
|
||||
let _ = write_stats_file(stats_path.as_ref(), &new_stats);
|
||||
}
|
||||
|
||||
if new_stats.usages < stats.usages {
|
||||
debug!("DETECTED lower usage count (new file or race with counter increasing): file {}", path.display());
|
||||
}
|
||||
}
|
||||
else {
|
||||
debug!("Can't read stats file again to update compression level (it might got cleaned up): file {}", stats_path.display());
|
||||
}
|
||||
});
|
||||
|
||||
trace!("Task finished: recompress file: {}", path.display());
|
||||
}
|
||||
|
||||
fn handle_on_cache_update(&self, path: PathBuf) {
|
||||
trace!("handle_on_cache_update() for path: {}", path.display());
|
||||
|
||||
// ---------------------- step 1: create .stats file
|
||||
|
||||
// construct .stats file path
|
||||
let filename = path
|
||||
.file_name()
|
||||
.expect("Expected valid cache file name")
|
||||
.to_str()
|
||||
.expect("Expected valid cache file name");
|
||||
let stats_path = path.with_file_name(format!("{}.stats", filename));
|
||||
|
||||
// create and write stats file
|
||||
let mut stats = ModuleCacheStatistics::default(&self.cache_config);
|
||||
stats.usages += 1;
|
||||
write_stats_file(&stats_path, &stats);
|
||||
|
||||
// ---------------------- step 2: perform cleanup task if needed
|
||||
|
||||
// acquire lock for cleanup task
|
||||
// Lock is a proof of recent cleanup task, so we don't want to delete them.
|
||||
// Expired locks will be deleted by the cleanup task.
|
||||
let cleanup_file = self.cache_config.directory().join(".cleanup"); // some non existing marker file
|
||||
if acquire_task_fs_lock(
|
||||
&cleanup_file,
|
||||
self.cache_config.cleanup_interval(),
|
||||
self.cache_config
|
||||
.allowed_clock_drift_for_files_from_future(),
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
trace!("Trying to clean up cache");
|
||||
|
||||
let mut cache_index = self.list_cache_contents();
|
||||
let future_tolerance = SystemTime::now()
|
||||
.checked_add(
|
||||
self.cache_config
|
||||
.allowed_clock_drift_for_files_from_future(),
|
||||
)
|
||||
.expect("Brace your cache, the next Big Bang is coming (time overflow)");
|
||||
cache_index.sort_unstable_by(|lhs, rhs| {
|
||||
// sort by age
|
||||
use CacheEntry::*;
|
||||
match (lhs, rhs) {
|
||||
(Recognized { mtime: lhs_mt, .. }, Recognized { mtime: rhs_mt, .. }) => {
|
||||
match (*lhs_mt > future_tolerance, *rhs_mt > future_tolerance) {
|
||||
// later == younger
|
||||
(false, false) => rhs_mt.cmp(lhs_mt),
|
||||
// files from far future are treated as oldest recognized files
|
||||
// we want to delete them, so the cache keeps track of recent files
|
||||
// however, we don't delete them uncodintionally,
|
||||
// because .stats file can be overwritten with a meaningful mtime
|
||||
(true, false) => cmp::Ordering::Greater,
|
||||
(false, true) => cmp::Ordering::Less,
|
||||
(true, true) => cmp::Ordering::Equal,
|
||||
}
|
||||
}
|
||||
// unrecognized is kind of infinity
|
||||
(Recognized { .. }, Unrecognized { .. }) => cmp::Ordering::Less,
|
||||
(Unrecognized { .. }, Recognized { .. }) => cmp::Ordering::Greater,
|
||||
(Unrecognized { .. }, Unrecognized { .. }) => cmp::Ordering::Equal,
|
||||
}
|
||||
});
|
||||
|
||||
// find "cut" boundary:
|
||||
// - remove unrecognized files anyway,
|
||||
// - remove some cache files if some quota has been exceeded
|
||||
let mut total_size = 0u64;
|
||||
let mut start_delete_idx = None;
|
||||
let mut start_delete_idx_if_deleting_recognized_items: Option<usize> = None;
|
||||
|
||||
let total_size_limit = self.cache_config.files_total_size_soft_limit();
|
||||
let file_count_limit = self.cache_config.file_count_soft_limit();
|
||||
let tsl_if_deleting = total_size_limit
|
||||
.checked_mul(
|
||||
self.cache_config
|
||||
.files_total_size_limit_percent_if_deleting() as u64,
|
||||
)
|
||||
.unwrap()
|
||||
/ 100;
|
||||
let fcl_if_deleting = file_count_limit
|
||||
.checked_mul(self.cache_config.file_count_limit_percent_if_deleting() as u64)
|
||||
.unwrap()
|
||||
/ 100;
|
||||
|
||||
for (idx, item) in cache_index.iter().enumerate() {
|
||||
let size = if let CacheEntry::Recognized { size, .. } = item {
|
||||
size
|
||||
} else {
|
||||
start_delete_idx = Some(idx);
|
||||
break;
|
||||
};
|
||||
|
||||
total_size += size;
|
||||
if start_delete_idx_if_deleting_recognized_items.is_none() {
|
||||
if total_size > tsl_if_deleting || (idx + 1) as u64 > fcl_if_deleting {
|
||||
start_delete_idx_if_deleting_recognized_items = Some(idx);
|
||||
}
|
||||
}
|
||||
|
||||
if total_size > total_size_limit || (idx + 1) as u64 > file_count_limit {
|
||||
start_delete_idx = start_delete_idx_if_deleting_recognized_items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = start_delete_idx {
|
||||
for item in &cache_index[idx..] {
|
||||
let (result, path, entity) = match item {
|
||||
CacheEntry::Recognized { path, .. }
|
||||
| CacheEntry::Unrecognized {
|
||||
path,
|
||||
is_dir: false,
|
||||
} => (fs::remove_file(path), path, "file"),
|
||||
CacheEntry::Unrecognized { path, is_dir: true } => {
|
||||
(fs::remove_dir_all(path), path, "directory")
|
||||
}
|
||||
};
|
||||
if let Err(err) = result {
|
||||
warn!(
|
||||
"Failed to remove {} during cleanup, path: {}, err: {}",
|
||||
entity,
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace!("Task finished: clean up cache");
|
||||
}
|
||||
|
||||
// Be fault tolerant: list as much as you can, and ignore the rest
|
||||
fn list_cache_contents(&self) -> Vec<CacheEntry> {
|
||||
fn enter_dir(
|
||||
vec: &mut Vec<CacheEntry>,
|
||||
dir_path: &Path,
|
||||
level: u8,
|
||||
cache_config: &CacheConfig,
|
||||
) {
|
||||
macro_rules! unwrap_or {
|
||||
($result:expr, $cont:stmt, $err_msg:expr) => {
|
||||
unwrap_or!($result, $cont, $err_msg, dir_path)
|
||||
};
|
||||
($result:expr, $cont:stmt, $err_msg:expr, $path:expr) => {
|
||||
match $result {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"{}, level: {}, path: {}, msg: {}",
|
||||
$err_msg,
|
||||
level,
|
||||
$path.display(),
|
||||
err
|
||||
);
|
||||
$cont
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! add_unrecognized {
|
||||
(file: $path:expr) => {
|
||||
add_unrecognized!(false, $path)
|
||||
};
|
||||
(dir: $path:expr) => {
|
||||
add_unrecognized!(true, $path)
|
||||
};
|
||||
($is_dir:expr, $path:expr) => {
|
||||
vec.push(CacheEntry::Unrecognized {
|
||||
path: $path.to_path_buf(),
|
||||
is_dir: $is_dir,
|
||||
});
|
||||
};
|
||||
}
|
||||
macro_rules! add_unrecognized_and {
|
||||
([ $( $ty:ident: $path:expr ),* ], $cont:stmt) => {{
|
||||
$( add_unrecognized!($ty: $path); )*
|
||||
$cont
|
||||
}};
|
||||
}
|
||||
|
||||
// If we fail to list a directory, something bad is happening anyway
|
||||
// (something touches our cache or we have disk failure)
|
||||
// Try to delete it, so we can stay within soft limits of the cache size.
|
||||
// This comment applies later in this function, too.
|
||||
let it = unwrap_or!(
|
||||
fs::read_dir(dir_path),
|
||||
add_unrecognized_and!([dir: dir_path], return),
|
||||
"Failed to list cache directory, deleting it"
|
||||
);
|
||||
|
||||
let mut cache_files = HashMap::new();
|
||||
for entry in it {
|
||||
// read_dir() returns an iterator over results - in case some of them are errors
|
||||
// we don't know their names, so we can't delete them. We don't want to delete
|
||||
// the whole directory with good entries too, so we just ignore the erroneous entries.
|
||||
let entry = unwrap_or!(
|
||||
entry,
|
||||
continue,
|
||||
"Failed to read a cache dir entry (NOT deleting it, it still occupies space)"
|
||||
);
|
||||
let path = entry.path();
|
||||
match (level, path.is_dir()) {
|
||||
(0..=1, true) => enter_dir(vec, &path, level + 1, cache_config),
|
||||
(0..=1, false) => {
|
||||
if level == 0 && path.file_stem() == Some(OsStr::new(".cleanup")) {
|
||||
if path.extension().is_some() {
|
||||
// assume it's cleanup lock
|
||||
if !is_fs_lock_expired(
|
||||
Some(&entry),
|
||||
&path,
|
||||
cache_config.cleanup_interval(),
|
||||
cache_config.allowed_clock_drift_for_files_from_future(),
|
||||
) {
|
||||
continue; // skip active lock
|
||||
}
|
||||
}
|
||||
}
|
||||
add_unrecognized!(file: path);
|
||||
}
|
||||
(2, false) => {
|
||||
let ext = path.extension();
|
||||
if ext.is_none() || ext == Some(OsStr::new("stats")) {
|
||||
// mod or stats file
|
||||
cache_files.insert(path, entry);
|
||||
} else {
|
||||
let recognized = if let Some(ext_str) = ext.unwrap().to_str() {
|
||||
// check if valid lock
|
||||
ext_str.starts_with("wip-")
|
||||
&& !is_fs_lock_expired(
|
||||
Some(&entry),
|
||||
&path,
|
||||
cache_config.optimizing_compression_task_timeout(),
|
||||
cache_config.allowed_clock_drift_for_files_from_future(),
|
||||
)
|
||||
} else {
|
||||
// if it's None, i.e. not valid UTF-8 string, then that's not our lock for sure
|
||||
false
|
||||
};
|
||||
|
||||
if !recognized {
|
||||
add_unrecognized!(file: path);
|
||||
}
|
||||
}
|
||||
}
|
||||
(_, is_dir) => add_unrecognized!(is_dir, path),
|
||||
}
|
||||
}
|
||||
|
||||
// associate module with its stats & handle them
|
||||
// assumption: just mods and stats
|
||||
for (path, entry) in cache_files.iter() {
|
||||
let path_buf: PathBuf;
|
||||
let (mod_, stats_, is_mod) = match path.extension() {
|
||||
Some(_) => {
|
||||
path_buf = path.with_extension("");
|
||||
(
|
||||
cache_files.get(&path_buf).map(|v| (&path_buf, v)),
|
||||
Some((path, entry)),
|
||||
false,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
path_buf = path.with_extension("stats");
|
||||
(
|
||||
Some((path, entry)),
|
||||
cache_files.get(&path_buf).map(|v| (&path_buf, v)),
|
||||
true,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// construct a cache entry
|
||||
match (mod_, stats_, is_mod) {
|
||||
(Some((mod_path, mod_entry)), Some((stats_path, stats_entry)), true) => {
|
||||
let mod_metadata = unwrap_or!(
|
||||
mod_entry.metadata(),
|
||||
add_unrecognized_and!([file: stats_path, file: mod_path], continue),
|
||||
"Failed to get metadata, deleting BOTH module cache and stats files",
|
||||
mod_path
|
||||
);
|
||||
let stats_mtime = unwrap_or!(
|
||||
stats_entry.metadata().and_then(|m| m.modified()),
|
||||
add_unrecognized_and!(
|
||||
[file: stats_path],
|
||||
unwrap_or!(
|
||||
mod_metadata.modified(),
|
||||
add_unrecognized_and!([file: stats_path, file: mod_path], continue),
|
||||
"Failed to get mtime, deleting BOTH module cache and stats files",
|
||||
mod_path
|
||||
)
|
||||
),
|
||||
"Failed to get metadata/mtime, deleting the file",
|
||||
stats_path
|
||||
);
|
||||
// .into() called for the SystemTimeStub if cfg(test)
|
||||
#[allow(clippy::identity_conversion)]
|
||||
vec.push(CacheEntry::Recognized {
|
||||
path: mod_path.to_path_buf(),
|
||||
mtime: stats_mtime.into(),
|
||||
size: mod_metadata.len(),
|
||||
})
|
||||
}
|
||||
(Some(_), Some(_), false) => (), // was or will be handled by previous branch
|
||||
(Some((mod_path, mod_entry)), None, _) => {
|
||||
let (mod_metadata, mod_mtime) = unwrap_or!(
|
||||
mod_entry
|
||||
.metadata()
|
||||
.and_then(|md| md.modified().map(|mt| (md, mt))),
|
||||
add_unrecognized_and!([file: mod_path], continue),
|
||||
"Failed to get metadata/mtime, deleting the file",
|
||||
mod_path
|
||||
);
|
||||
// .into() called for the SystemTimeStub if cfg(test)
|
||||
#[allow(clippy::identity_conversion)]
|
||||
vec.push(CacheEntry::Recognized {
|
||||
path: mod_path.to_path_buf(),
|
||||
mtime: mod_mtime.into(),
|
||||
size: mod_metadata.len(),
|
||||
})
|
||||
}
|
||||
(None, Some((stats_path, _stats_entry)), _) => {
|
||||
debug!("Found orphaned stats file: {}", stats_path.display());
|
||||
add_unrecognized!(file: stats_path);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut vec = Vec::new();
|
||||
enter_dir(
|
||||
&mut vec,
|
||||
self.cache_config.directory(),
|
||||
0,
|
||||
&self.cache_config,
|
||||
);
|
||||
vec
|
||||
}
|
||||
}
|
||||
|
||||
fn read_stats_file(path: &Path) -> Option<ModuleCacheStatistics> {
|
||||
fs::read(path)
|
||||
.map_err(|err| {
|
||||
trace!(
|
||||
"Failed to read stats file, path: {}, err: {}",
|
||||
path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.and_then(|bytes| {
|
||||
toml::from_slice::<ModuleCacheStatistics>(&bytes[..]).map_err(|err| {
|
||||
trace!(
|
||||
"Failed to parse stats file, path: {}, err: {}",
|
||||
path.display(),
|
||||
err,
|
||||
)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn write_stats_file(path: &Path, stats: &ModuleCacheStatistics) -> bool {
|
||||
toml::to_string_pretty(&stats)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to serialize stats file, path: {}, err: {}",
|
||||
path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.and_then(|serialized| {
|
||||
if fs_write_atomic(path, "stats", serialized.as_bytes()) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Tries to acquire a lock for specific task.
|
||||
///
|
||||
/// Returns Some(path) to the lock if succeeds. The task path must not
|
||||
/// contain any extension and have file stem.
|
||||
///
|
||||
/// To release a lock you need either manually rename or remove it,
|
||||
/// or wait until it expires and cleanup task removes it.
|
||||
///
|
||||
/// Note: this function is racy. Main idea is: be fault tolerant and
|
||||
/// never block some task. The price is that we rarely do some task
|
||||
/// more than once.
|
||||
fn acquire_task_fs_lock(
|
||||
task_path: &Path,
|
||||
timeout: Duration,
|
||||
allowed_future_drift: Duration,
|
||||
) -> Option<PathBuf> {
|
||||
assert!(task_path.extension().is_none());
|
||||
assert!(task_path.file_stem().is_some());
|
||||
|
||||
// list directory
|
||||
let dir_path = task_path.parent()?;
|
||||
let it = fs::read_dir(dir_path)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to list cache directory, path: {}, err: {}",
|
||||
dir_path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
// look for existing locks
|
||||
for entry in it {
|
||||
let entry = entry
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to list cache directory, path: {}, err: {}",
|
||||
dir_path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let path = entry.path();
|
||||
if path.is_dir() || path.file_stem() != task_path.file_stem() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check extension and mtime
|
||||
match path.extension() {
|
||||
None => continue,
|
||||
Some(ext) => {
|
||||
if let Some(ext_str) = ext.to_str() {
|
||||
// if it's None, i.e. not valid UTF-8 string, then that's not our lock for sure
|
||||
if ext_str.starts_with("wip-")
|
||||
&& !is_fs_lock_expired(Some(&entry), &path, timeout, allowed_future_drift)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create the lock
|
||||
let lock_path = task_path.with_extension(format!("wip-{}", std::process::id()));
|
||||
let _file = fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&lock_path)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to create lock file (note: it shouldn't exists): path: {}, err: {}",
|
||||
lock_path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
Some(lock_path)
|
||||
}
|
||||
|
||||
// we have either both, or just path; dir entry is desirable since on some platforms we can get
|
||||
// metadata without extra syscalls
|
||||
// futhermore: it's better to get a path if we have it instead of allocating a new one from the dir entry
|
||||
fn is_fs_lock_expired(
|
||||
entry: Option<&fs::DirEntry>,
|
||||
path: &PathBuf,
|
||||
threshold: Duration,
|
||||
allowed_future_drift: Duration,
|
||||
) -> bool {
|
||||
let mtime = match entry
|
||||
.map_or_else(|| path.metadata(), |e| e.metadata())
|
||||
.and_then(|metadata| metadata.modified())
|
||||
{
|
||||
Ok(mt) => mt,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Failed to get metadata/mtime, treating as an expired lock, path: {}, err: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
return true; // can't read mtime, treat as expired, so this task will not be starved
|
||||
}
|
||||
};
|
||||
|
||||
// DON'T use: mtime.elapsed() -- we must call SystemTime directly for the tests to be deterministic
|
||||
match SystemTime::now().duration_since(mtime) {
|
||||
Ok(elapsed) => elapsed >= threshold,
|
||||
Err(err) => {
|
||||
trace!(
|
||||
"Found mtime in the future, treating as a not expired lock, path: {}, err: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
// the lock is expired if the time is too far in the future
|
||||
// it is fine to have network share and not synchronized clocks,
|
||||
// but it's not good when user changes time in their system clock
|
||||
err.duration() > allowed_future_drift
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
758
crates/environ/src/cache/worker/tests.rs
vendored
Normal file
758
crates/environ/src/cache/worker/tests.rs
vendored
Normal file
@@ -0,0 +1,758 @@
|
||||
use super::*;
|
||||
use crate::cache::config::tests::test_prolog;
|
||||
use core::iter::repeat;
|
||||
use std::process;
|
||||
// load_config! comes from crate::cache(::config::tests);
|
||||
|
||||
// when doing anything with the tests, make sure they are DETERMINISTIC
|
||||
// -- the result shouldn't rely on system time!
|
||||
pub mod system_time_stub;
|
||||
|
||||
#[test]
|
||||
fn test_on_get_create_stats_file() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
worker.on_cache_get_async(mod_file);
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, 1);
|
||||
assert_eq!(
|
||||
stats.compression_level,
|
||||
cache_config.baseline_compression_level()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_get_update_usage_counter() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let default_stats = ModuleCacheStatistics::default(&cache_config);
|
||||
assert!(write_stats_file(&stats_file, &default_stats));
|
||||
|
||||
let mut usages = 0;
|
||||
for times_used in &[4, 7, 2] {
|
||||
for _ in 0..*times_used {
|
||||
worker.on_cache_get_async(mod_file.clone());
|
||||
usages += 1;
|
||||
}
|
||||
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, usages);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_get_recompress_no_mod_file() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level = 7\n\
|
||||
optimized-compression-usage-counter-threshold = '256'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let mut start_stats = ModuleCacheStatistics::default(&cache_config);
|
||||
start_stats.usages = 250;
|
||||
assert!(write_stats_file(&stats_file, &start_stats));
|
||||
|
||||
let mut usages = start_stats.usages;
|
||||
for times_used in &[4, 7, 2] {
|
||||
for _ in 0..*times_used {
|
||||
worker.on_cache_get_async(mod_file.clone());
|
||||
usages += 1;
|
||||
}
|
||||
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, usages);
|
||||
assert_eq!(
|
||||
stats.compression_level,
|
||||
cache_config.baseline_compression_level()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_get_recompress_with_mod_file() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level = 7\n\
|
||||
optimized-compression-usage-counter-threshold = '256'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let mod_data = "some test data to be compressed";
|
||||
let data = zstd::encode_all(
|
||||
mod_data.as_bytes(),
|
||||
cache_config.baseline_compression_level(),
|
||||
)
|
||||
.expect("Failed to compress sample mod file");
|
||||
fs::write(&mod_file, &data).expect("Failed to write sample mod file");
|
||||
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let mut start_stats = ModuleCacheStatistics::default(&cache_config);
|
||||
start_stats.usages = 250;
|
||||
assert!(write_stats_file(&stats_file, &start_stats));
|
||||
|
||||
// scenarios:
|
||||
// 1. Shouldn't be recompressed
|
||||
// 2. Should be recompressed
|
||||
// 3. After lowering compression level, should be recompressed
|
||||
let scenarios = [(4, false), (7, true), (2, false)];
|
||||
|
||||
let mut usages = start_stats.usages;
|
||||
assert!(usages < cache_config.optimized_compression_usage_counter_threshold());
|
||||
let mut tested_higher_opt_compr_lvl = false;
|
||||
for (times_used, lower_compr_lvl) in &scenarios {
|
||||
for _ in 0..*times_used {
|
||||
worker.on_cache_get_async(mod_file.clone());
|
||||
usages += 1;
|
||||
}
|
||||
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let mut stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, usages);
|
||||
assert_eq!(
|
||||
stats.compression_level,
|
||||
if usages < cache_config.optimized_compression_usage_counter_threshold() {
|
||||
cache_config.baseline_compression_level()
|
||||
} else {
|
||||
cache_config.optimized_compression_level()
|
||||
}
|
||||
);
|
||||
let compressed_data = fs::read(&mod_file).expect("Failed to read mod file");
|
||||
let decoded_data =
|
||||
zstd::decode_all(&compressed_data[..]).expect("Failed to decompress mod file");
|
||||
assert_eq!(decoded_data, mod_data.as_bytes());
|
||||
|
||||
if *lower_compr_lvl {
|
||||
assert!(usages >= cache_config.optimized_compression_usage_counter_threshold());
|
||||
tested_higher_opt_compr_lvl = true;
|
||||
stats.compression_level -= 1;
|
||||
assert!(write_stats_file(&stats_file, &stats));
|
||||
}
|
||||
}
|
||||
assert!(usages >= cache_config.optimized_compression_usage_counter_threshold());
|
||||
assert!(tested_higher_opt_compr_lvl);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_get_recompress_lock() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level = 7\n\
|
||||
optimized-compression-usage-counter-threshold = '256'\n\
|
||||
optimizing-compression-task-timeout = '30m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1d'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let mod_data = "some test data to be compressed";
|
||||
let data = zstd::encode_all(
|
||||
mod_data.as_bytes(),
|
||||
cache_config.baseline_compression_level(),
|
||||
)
|
||||
.expect("Failed to compress sample mod file");
|
||||
fs::write(&mod_file, &data).expect("Failed to write sample mod file");
|
||||
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let mut start_stats = ModuleCacheStatistics::default(&cache_config);
|
||||
start_stats.usages = 255;
|
||||
|
||||
let lock_file = cache_dir.join("some-mod.wip-lock");
|
||||
|
||||
let scenarios = [
|
||||
// valid lock
|
||||
(true, "past", Duration::from_secs(30 * 60 - 1)),
|
||||
// valid future lock
|
||||
(true, "future", Duration::from_secs(24 * 60 * 60)),
|
||||
// expired lock
|
||||
(false, "past", Duration::from_secs(30 * 60)),
|
||||
// expired future lock
|
||||
(false, "future", Duration::from_secs(24 * 60 * 60 + 1)),
|
||||
];
|
||||
|
||||
for (lock_valid, duration_sign, duration) in &scenarios {
|
||||
assert!(write_stats_file(&stats_file, &start_stats)); // restore usage & compression level
|
||||
create_file_with_mtime(&lock_file, "", duration_sign, &duration);
|
||||
|
||||
worker.on_cache_get_async(mod_file.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, start_stats.usages + 1);
|
||||
assert_eq!(
|
||||
stats.compression_level,
|
||||
if *lock_valid {
|
||||
cache_config.baseline_compression_level()
|
||||
} else {
|
||||
cache_config.optimized_compression_level()
|
||||
}
|
||||
);
|
||||
let compressed_data = fs::read(&mod_file).expect("Failed to read mod file");
|
||||
let decoded_data =
|
||||
zstd::decode_all(&compressed_data[..]).expect("Failed to decompress mod file");
|
||||
assert_eq!(decoded_data, mod_data.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_update_fresh_stats_file() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level = 7\n\
|
||||
cleanup-interval = '1h'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let cleanup_certificate = cache_dir.join(".cleanup.wip-done");
|
||||
create_file_with_mtime(&cleanup_certificate, "", "future", &Duration::from_secs(0));
|
||||
// the below created by the worker if it cleans up
|
||||
let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
|
||||
|
||||
// scenarios:
|
||||
// 1. Create new stats file
|
||||
// 2. Overwrite existing file
|
||||
for update_file in &[true, false] {
|
||||
worker.on_cache_update_async(mod_file.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let mut stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, 1);
|
||||
assert_eq!(
|
||||
stats.compression_level,
|
||||
cache_config.baseline_compression_level()
|
||||
);
|
||||
|
||||
if *update_file {
|
||||
stats.usages += 42;
|
||||
stats.compression_level += 1;
|
||||
assert!(write_stats_file(&stats_file, &stats));
|
||||
}
|
||||
|
||||
assert!(!worker_lock_file.exists());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_update_cleanup_limits_trash_locks() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
cleanup-interval = '30m'\n\
|
||||
optimizing-compression-task-timeout = '30m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1d'\n\
|
||||
file-count-soft-limit = '5'\n\
|
||||
files-total-size-soft-limit = '30K'\n\
|
||||
file-count-limit-percent-if-deleting = '70%'\n\
|
||||
files-total-size-limit-percent-if-deleting = '70%'
|
||||
",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
let content_1k = "a".repeat(1_000);
|
||||
let content_10k = "a".repeat(10_000);
|
||||
|
||||
let mods_files_dir = cache_dir.join("target-triple").join("compiler-version");
|
||||
let mod_with_stats = mods_files_dir.join("mod-with-stats");
|
||||
let trash_dirs = [
|
||||
mods_files_dir.join("trash"),
|
||||
mods_files_dir.join("trash").join("trash"),
|
||||
];
|
||||
let trash_files = [
|
||||
cache_dir.join("trash-file"),
|
||||
cache_dir.join("trash-file.wip-lock"),
|
||||
cache_dir.join("target-triple").join("trash.txt"),
|
||||
cache_dir.join("target-triple").join("trash.txt.wip-lock"),
|
||||
mods_files_dir.join("trash.ogg"),
|
||||
mods_files_dir.join("trash").join("trash.doc"),
|
||||
mods_files_dir.join("trash").join("trash.doc.wip-lock"),
|
||||
mods_files_dir.join("trash").join("trash").join("trash.xls"),
|
||||
mods_files_dir
|
||||
.join("trash")
|
||||
.join("trash")
|
||||
.join("trash.xls.wip-lock"),
|
||||
];
|
||||
let mod_locks = [
|
||||
// valid lock
|
||||
(
|
||||
mods_files_dir.join("mod0.wip-lock"),
|
||||
true,
|
||||
"past",
|
||||
Duration::from_secs(30 * 60 - 1),
|
||||
),
|
||||
// valid future lock
|
||||
(
|
||||
mods_files_dir.join("mod1.wip-lock"),
|
||||
true,
|
||||
"future",
|
||||
Duration::from_secs(24 * 60 * 60),
|
||||
),
|
||||
// expired lock
|
||||
(
|
||||
mods_files_dir.join("mod2.wip-lock"),
|
||||
false,
|
||||
"past",
|
||||
Duration::from_secs(30 * 60),
|
||||
),
|
||||
// expired future lock
|
||||
(
|
||||
mods_files_dir.join("mod3.wip-lock"),
|
||||
false,
|
||||
"future",
|
||||
Duration::from_secs(24 * 60 * 60 + 1),
|
||||
),
|
||||
];
|
||||
// the below created by the worker if it cleans up
|
||||
let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
|
||||
|
||||
let scenarios = [
|
||||
// Close to limits, but not reached, only trash deleted
|
||||
(2, 2, 4),
|
||||
// File count limit exceeded
|
||||
(1, 10, 3),
|
||||
// Total size limit exceeded
|
||||
(4, 0, 2),
|
||||
// Both limits exceeded
|
||||
(3, 5, 3),
|
||||
];
|
||||
|
||||
for (files_10k, files_1k, remaining_files) in &scenarios {
|
||||
let mut secs_ago = 100;
|
||||
|
||||
for d in &trash_dirs {
|
||||
fs::create_dir_all(d).expect("Failed to create directories");
|
||||
}
|
||||
for f in &trash_files {
|
||||
create_file_with_mtime(f, "", "past", &Duration::from_secs(0));
|
||||
}
|
||||
for (f, _, sign, duration) in &mod_locks {
|
||||
create_file_with_mtime(f, "", sign, &duration);
|
||||
}
|
||||
|
||||
let mut mods_paths = vec![];
|
||||
for content in repeat(&content_10k)
|
||||
.take(*files_10k)
|
||||
.chain(repeat(&content_1k).take(*files_1k))
|
||||
{
|
||||
mods_paths.push(mods_files_dir.join(format!("test-mod-{}", mods_paths.len())));
|
||||
create_file_with_mtime(
|
||||
mods_paths.last().unwrap(),
|
||||
content,
|
||||
"past",
|
||||
&Duration::from_secs(secs_ago),
|
||||
);
|
||||
assert!(secs_ago > 0);
|
||||
secs_ago -= 1;
|
||||
}
|
||||
|
||||
// creating .stats file updates mtime what affects test results
|
||||
// so we use a separate nonexistent module here (orphaned .stats will be removed anyway)
|
||||
worker.on_cache_update_async(mod_with_stats.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
for ent in trash_dirs.iter().chain(trash_files.iter()) {
|
||||
assert!(!ent.exists());
|
||||
}
|
||||
for (f, valid, ..) in &mod_locks {
|
||||
assert_eq!(f.exists(), *valid);
|
||||
}
|
||||
for (idx, path) in mods_paths.iter().enumerate() {
|
||||
let should_exist = idx >= mods_paths.len() - *remaining_files;
|
||||
assert_eq!(path.exists(), should_exist);
|
||||
if should_exist {
|
||||
// cleanup before next iteration
|
||||
fs::remove_file(path).expect("Failed to remove a file");
|
||||
}
|
||||
}
|
||||
fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_update_cleanup_lru_policy() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
file-count-soft-limit = '5'\n\
|
||||
files-total-size-soft-limit = '30K'\n\
|
||||
file-count-limit-percent-if-deleting = '80%'\n\
|
||||
files-total-size-limit-percent-if-deleting = '70%'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
let content_1k = "a".repeat(1_000);
|
||||
let content_5k = "a".repeat(5_000);
|
||||
let content_10k = "a".repeat(10_000);
|
||||
|
||||
let mods_files_dir = cache_dir.join("target-triple").join("compiler-version");
|
||||
fs::create_dir_all(&mods_files_dir).expect("Failed to create directories");
|
||||
let nonexistent_mod_file = cache_dir.join("nonexistent-mod");
|
||||
let orphaned_stats_file = cache_dir.join("orphaned-mod.stats");
|
||||
let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
|
||||
|
||||
// content, how long ago created, how long ago stats created (if created), should be alive
|
||||
let scenarios = [
|
||||
&[
|
||||
(&content_10k, 29, None, false),
|
||||
(&content_10k, 28, None, false),
|
||||
(&content_10k, 27, None, false),
|
||||
(&content_1k, 26, None, true),
|
||||
(&content_10k, 25, None, true),
|
||||
(&content_1k, 24, None, true),
|
||||
],
|
||||
&[
|
||||
(&content_10k, 29, None, false),
|
||||
(&content_10k, 28, None, false),
|
||||
(&content_10k, 27, None, true),
|
||||
(&content_1k, 26, None, true),
|
||||
(&content_5k, 25, None, true),
|
||||
(&content_1k, 24, None, true),
|
||||
],
|
||||
&[
|
||||
(&content_10k, 29, Some(19), true),
|
||||
(&content_10k, 28, None, false),
|
||||
(&content_10k, 27, None, false),
|
||||
(&content_1k, 26, Some(18), true),
|
||||
(&content_5k, 25, None, true),
|
||||
(&content_1k, 24, None, true),
|
||||
],
|
||||
&[
|
||||
(&content_10k, 29, Some(19), true),
|
||||
(&content_10k, 28, Some(18), true),
|
||||
(&content_10k, 27, None, false),
|
||||
(&content_1k, 26, Some(17), true),
|
||||
(&content_5k, 25, None, false),
|
||||
(&content_1k, 24, None, false),
|
||||
],
|
||||
&[
|
||||
(&content_10k, 29, Some(19), true),
|
||||
(&content_10k, 28, None, false),
|
||||
(&content_1k, 27, None, false),
|
||||
(&content_5k, 26, Some(18), true),
|
||||
(&content_1k, 25, None, false),
|
||||
(&content_10k, 24, None, false),
|
||||
],
|
||||
];
|
||||
|
||||
for mods in &scenarios {
|
||||
let filenames = (0..mods.len())
|
||||
.map(|i| {
|
||||
(
|
||||
mods_files_dir.join(format!("mod-{}", i)),
|
||||
mods_files_dir.join(format!("mod-{}.stats", i)),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for ((content, mod_secs_ago, create_stats, _), (mod_filename, stats_filename)) in
|
||||
mods.iter().zip(filenames.iter())
|
||||
{
|
||||
create_file_with_mtime(
|
||||
mod_filename,
|
||||
content,
|
||||
"past",
|
||||
&Duration::from_secs(*mod_secs_ago),
|
||||
);
|
||||
if let Some(stats_secs_ago) = create_stats {
|
||||
create_file_with_mtime(
|
||||
stats_filename,
|
||||
"cleanup doesn't care",
|
||||
"past",
|
||||
&Duration::from_secs(*stats_secs_ago),
|
||||
);
|
||||
}
|
||||
}
|
||||
create_file_with_mtime(
|
||||
&orphaned_stats_file,
|
||||
"cleanup doesn't care",
|
||||
"past",
|
||||
&Duration::from_secs(0),
|
||||
);
|
||||
|
||||
worker.on_cache_update_async(nonexistent_mod_file.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
assert!(!orphaned_stats_file.exists());
|
||||
for ((_, _, create_stats, alive), (mod_filename, stats_filename)) in
|
||||
mods.iter().zip(filenames.iter())
|
||||
{
|
||||
assert_eq!(mod_filename.exists(), *alive);
|
||||
assert_eq!(stats_filename.exists(), *alive && create_stats.is_some());
|
||||
|
||||
// cleanup for next iteration
|
||||
if *alive {
|
||||
fs::remove_file(&mod_filename).expect("Failed to remove a file");
|
||||
if create_stats.is_some() {
|
||||
fs::remove_file(&stats_filename).expect("Failed to remove a file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
|
||||
}
|
||||
}
|
||||
|
||||
// clock drift should be applied to mod cache & stats, too
|
||||
// however, postpone deleting files to as late as possible
|
||||
#[test]
|
||||
fn test_on_update_cleanup_future_files() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1d'\n\
|
||||
file-count-soft-limit = '3'\n\
|
||||
files-total-size-soft-limit = '1M'\n\
|
||||
file-count-limit-percent-if-deleting = '70%'\n\
|
||||
files-total-size-limit-percent-if-deleting = '70%'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
let content_1k = "a".repeat(1_000);
|
||||
|
||||
let mods_files_dir = cache_dir.join("target-triple").join("compiler-version");
|
||||
fs::create_dir_all(&mods_files_dir).expect("Failed to create directories");
|
||||
let nonexistent_mod_file = cache_dir.join("nonexistent-mod");
|
||||
// the below created by the worker if it cleans up
|
||||
let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
|
||||
|
||||
let scenarios: [&[_]; 5] = [
|
||||
// NOT cleaning up, everythings ok
|
||||
&[
|
||||
(Duration::from_secs(0), None, true),
|
||||
(Duration::from_secs(24 * 60 * 60), None, true),
|
||||
],
|
||||
// NOT cleaning up, everythings ok
|
||||
&[
|
||||
(Duration::from_secs(0), None, true),
|
||||
(Duration::from_secs(24 * 60 * 60 + 1), None, true),
|
||||
],
|
||||
// cleaning up, removing files from oldest
|
||||
&[
|
||||
(Duration::from_secs(0), None, false),
|
||||
(Duration::from_secs(24 * 60 * 60), None, true),
|
||||
(Duration::from_secs(1), None, false),
|
||||
(Duration::from_secs(2), None, true),
|
||||
],
|
||||
// cleaning up, removing files from oldest; deleting file from far future
|
||||
&[
|
||||
(Duration::from_secs(0), None, false),
|
||||
(Duration::from_secs(1), None, true),
|
||||
(Duration::from_secs(24 * 60 * 60 + 1), None, false),
|
||||
(Duration::from_secs(2), None, true),
|
||||
],
|
||||
// cleaning up, removing files from oldest; file from far future should have .stats from +-now => it's a legitimate file
|
||||
&[
|
||||
(Duration::from_secs(0), None, false),
|
||||
(Duration::from_secs(1), None, false),
|
||||
(
|
||||
Duration::from_secs(24 * 60 * 60 + 1),
|
||||
Some(Duration::from_secs(3)),
|
||||
true,
|
||||
),
|
||||
(Duration::from_secs(2), None, true),
|
||||
],
|
||||
];
|
||||
|
||||
for mods in &scenarios {
|
||||
let filenames = (0..mods.len())
|
||||
.map(|i| {
|
||||
(
|
||||
mods_files_dir.join(format!("mod-{}", i)),
|
||||
mods_files_dir.join(format!("mod-{}.stats", i)),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for ((duration, opt_stats_duration, _), (mod_filename, stats_filename)) in
|
||||
mods.iter().zip(filenames.iter())
|
||||
{
|
||||
create_file_with_mtime(mod_filename, &content_1k, "future", duration);
|
||||
if let Some(stats_duration) = opt_stats_duration {
|
||||
create_file_with_mtime(stats_filename, "", "future", stats_duration);
|
||||
}
|
||||
}
|
||||
|
||||
worker.on_cache_update_async(nonexistent_mod_file.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
for ((_, opt_stats_duration, alive), (mod_filename, stats_filename)) in
|
||||
mods.iter().zip(filenames.iter())
|
||||
{
|
||||
assert_eq!(mod_filename.exists(), *alive);
|
||||
assert_eq!(
|
||||
stats_filename.exists(),
|
||||
*alive && opt_stats_duration.is_some()
|
||||
);
|
||||
if *alive {
|
||||
fs::remove_file(mod_filename).expect("Failed to remove a file");
|
||||
if opt_stats_duration.is_some() {
|
||||
fs::remove_file(stats_filename).expect("Failed to remove a file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
|
||||
}
|
||||
}
|
||||
|
||||
// this tests if worker triggered cleanup or not when some cleanup lock/certificate was out there
|
||||
#[test]
|
||||
fn test_on_update_cleanup_self_lock() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
cleanup-interval = '30m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1d'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let trash_file = cache_dir.join("trash-file.txt");
|
||||
|
||||
let lock_file = cache_dir.join(".cleanup.wip-lock");
|
||||
// the below created by the worker if it cleans up
|
||||
let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
|
||||
|
||||
let scenarios = [
|
||||
// valid lock
|
||||
(true, "past", Duration::from_secs(30 * 60 - 1)),
|
||||
// valid future lock
|
||||
(true, "future", Duration::from_secs(24 * 60 * 60)),
|
||||
// expired lock
|
||||
(false, "past", Duration::from_secs(30 * 60)),
|
||||
// expired future lock
|
||||
(false, "future", Duration::from_secs(24 * 60 * 60 + 1)),
|
||||
];
|
||||
|
||||
for (lock_valid, duration_sign, duration) in &scenarios {
|
||||
create_file_with_mtime(
|
||||
&trash_file,
|
||||
"with trash content",
|
||||
"future",
|
||||
&Duration::from_secs(0),
|
||||
);
|
||||
create_file_with_mtime(&lock_file, "", duration_sign, &duration);
|
||||
|
||||
worker.on_cache_update_async(mod_file.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
assert_eq!(trash_file.exists(), *lock_valid);
|
||||
assert_eq!(lock_file.exists(), *lock_valid);
|
||||
if *lock_valid {
|
||||
assert!(!worker_lock_file.exists());
|
||||
} else {
|
||||
fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file_with_mtime(filename: &Path, contents: &str, offset_sign: &str, offset: &Duration) {
|
||||
fs::write(filename, contents).expect("Failed to create a file");
|
||||
let mtime = match offset_sign {
|
||||
"past" => system_time_stub::NOW
|
||||
.checked_sub(*offset)
|
||||
.expect("Failed to calculate new mtime"),
|
||||
"future" => system_time_stub::NOW
|
||||
.checked_add(*offset)
|
||||
.expect("Failed to calculate new mtime"),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
filetime::set_file_mtime(filename, mtime.into()).expect("Failed to set mtime");
|
||||
}
|
||||
29
crates/environ/src/cache/worker/tests/system_time_stub.rs
vendored
Normal file
29
crates/environ/src/cache/worker/tests/system_time_stub.rs
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
use lazy_static::lazy_static;
|
||||
use std::time::{Duration, SystemTime, SystemTimeError};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref NOW: SystemTime = SystemTime::now(); // no need for RefCell and set_now() for now
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Ord, Eq)]
|
||||
pub struct SystemTimeStub(SystemTime);
|
||||
|
||||
impl SystemTimeStub {
|
||||
pub fn now() -> Self {
|
||||
Self(*NOW)
|
||||
}
|
||||
|
||||
pub fn checked_add(&self, duration: Duration) -> Option<Self> {
|
||||
self.0.checked_add(duration).map(|t| t.into())
|
||||
}
|
||||
|
||||
pub fn duration_since(&self, earlier: SystemTime) -> Result<Duration, SystemTimeError> {
|
||||
self.0.duration_since(earlier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemTime> for SystemTimeStub {
|
||||
fn from(time: SystemTime) -> Self {
|
||||
Self(time)
|
||||
}
|
||||
}
|
||||
187
crates/environ/src/compilation.rs
Normal file
187
crates/environ/src/compilation.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
//! A `Compilation` contains the compiled function bodies for a WebAssembly
|
||||
//! module.
|
||||
|
||||
use crate::address_map::{ModuleAddressMap, ValueLabelsRanges};
|
||||
use crate::module;
|
||||
use crate::module_environ::FunctionBodyData;
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_codegen::{binemit, ir, isa, CodegenError};
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::{DefinedFuncIndex, FuncIndex, ModuleTranslationState, WasmError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Range;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Compiled function: machine code body, jump table offsets, and unwind information.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CompiledFunction {
|
||||
/// The function body.
|
||||
pub body: Vec<u8>,
|
||||
|
||||
/// The jump tables offsets (in the body).
|
||||
pub jt_offsets: ir::JumpTableOffsets,
|
||||
|
||||
/// The unwind information.
|
||||
pub unwind_info: Vec<u8>,
|
||||
}
|
||||
|
||||
type Functions = PrimaryMap<DefinedFuncIndex, CompiledFunction>;
|
||||
|
||||
/// The result of compiling a WebAssembly module's functions.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
|
||||
pub struct Compilation {
|
||||
/// Compiled machine code for the function bodies.
|
||||
functions: Functions,
|
||||
}
|
||||
|
||||
impl Compilation {
|
||||
/// Creates a compilation artifact from a contiguous function buffer and a set of ranges
|
||||
pub fn new(functions: Functions) -> Self {
|
||||
Self { functions }
|
||||
}
|
||||
|
||||
/// Allocates the compilation result with the given function bodies.
|
||||
pub fn from_buffer(
|
||||
buffer: Vec<u8>,
|
||||
functions: impl IntoIterator<Item = (Range<usize>, ir::JumpTableOffsets, Range<usize>)>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
functions
|
||||
.into_iter()
|
||||
.map(|(body_range, jt_offsets, unwind_range)| CompiledFunction {
|
||||
body: buffer[body_range].to_vec(),
|
||||
jt_offsets,
|
||||
unwind_info: buffer[unwind_range].to_vec(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets the bytes of a single function
|
||||
pub fn get(&self, func: DefinedFuncIndex) -> &CompiledFunction {
|
||||
&self.functions[func]
|
||||
}
|
||||
|
||||
/// Gets the number of functions defined.
|
||||
pub fn len(&self) -> usize {
|
||||
self.functions.len()
|
||||
}
|
||||
|
||||
/// Gets functions jump table offsets.
|
||||
pub fn get_jt_offsets(&self) -> PrimaryMap<DefinedFuncIndex, ir::JumpTableOffsets> {
|
||||
self.functions
|
||||
.iter()
|
||||
.map(|(_, func)| func.jt_offsets.clone())
|
||||
.collect::<PrimaryMap<DefinedFuncIndex, _>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Compilation {
|
||||
type IntoIter = Iter<'a>;
|
||||
type Item = <Self::IntoIter as Iterator>::Item;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Iter {
|
||||
iterator: self.functions.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Iter<'a> {
|
||||
iterator: <&'a Functions as IntoIterator>::IntoIter,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = &'a CompiledFunction;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iterator.next().map(|(_, b)| b)
|
||||
}
|
||||
}
|
||||
|
||||
/// A record of a relocation to perform.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Relocation {
|
||||
/// The relocation code.
|
||||
pub reloc: binemit::Reloc,
|
||||
/// Relocation target.
|
||||
pub reloc_target: RelocationTarget,
|
||||
/// The offset where to apply the relocation.
|
||||
pub offset: binemit::CodeOffset,
|
||||
/// The addend to add to the relocation value.
|
||||
pub addend: binemit::Addend,
|
||||
}
|
||||
|
||||
/// Destination function. Can be either user function or some special one, like `memory.grow`.
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum RelocationTarget {
|
||||
/// The user function index.
|
||||
UserFunc(FuncIndex),
|
||||
/// A compiler-generated libcall.
|
||||
LibCall(ir::LibCall),
|
||||
/// Function for growing a locally-defined 32-bit memory by the specified amount of pages.
|
||||
Memory32Grow,
|
||||
/// Function for growing an imported 32-bit memory by the specified amount of pages.
|
||||
ImportedMemory32Grow,
|
||||
/// Function for query current size of a locally-defined 32-bit linear memory.
|
||||
Memory32Size,
|
||||
/// Function for query current size of an imported 32-bit linear memory.
|
||||
ImportedMemory32Size,
|
||||
/// Jump table index.
|
||||
JumpTable(FuncIndex, ir::JumpTable),
|
||||
}
|
||||
|
||||
/// Relocations to apply to function bodies.
|
||||
pub type Relocations = PrimaryMap<DefinedFuncIndex, Vec<Relocation>>;
|
||||
|
||||
/// Information about trap.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct TrapInformation {
|
||||
/// The offset of the trapping instruction in native code. It is relative to the beginning of the function.
|
||||
pub code_offset: binemit::CodeOffset,
|
||||
/// Location of trapping instruction in WebAssembly binary module.
|
||||
pub source_loc: ir::SourceLoc,
|
||||
/// Code of the trap.
|
||||
pub trap_code: ir::TrapCode,
|
||||
}
|
||||
|
||||
/// Information about traps associated with the functions where the traps are placed.
|
||||
pub type Traps = PrimaryMap<DefinedFuncIndex, Vec<TrapInformation>>;
|
||||
|
||||
/// An error while compiling WebAssembly to machine code.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CompileError {
|
||||
/// A wasm translation error occured.
|
||||
#[error("WebAssembly translation error: {0}")]
|
||||
Wasm(#[from] WasmError),
|
||||
|
||||
/// A compilation error occured.
|
||||
#[error("Compilation error: {0}")]
|
||||
Codegen(#[from] CodegenError),
|
||||
|
||||
/// A compilation error occured.
|
||||
#[error("Debug info is not supported with this configuration")]
|
||||
DebugInfoNotSupported,
|
||||
}
|
||||
|
||||
/// An implementation of a compiler from parsed WebAssembly module to native code.
|
||||
pub trait Compiler {
|
||||
/// Compile a parsed module with the given `TargetIsa`.
|
||||
fn compile_module<'data, 'module>(
|
||||
module: &'module module::Module,
|
||||
module_translation: &ModuleTranslationState,
|
||||
function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
generate_debug_info: bool,
|
||||
) -> Result<
|
||||
(
|
||||
Compilation,
|
||||
Relocations,
|
||||
ModuleAddressMap,
|
||||
ValueLabelsRanges,
|
||||
PrimaryMap<DefinedFuncIndex, ir::StackSlots>,
|
||||
Traps,
|
||||
),
|
||||
CompileError,
|
||||
>;
|
||||
}
|
||||
322
crates/environ/src/cranelift.rs
Normal file
322
crates/environ/src/cranelift.rs
Normal file
@@ -0,0 +1,322 @@
|
||||
//! Support for compiling with Cranelift.
|
||||
|
||||
use crate::address_map::{
|
||||
FunctionAddressMap, InstructionAddressMap, ModuleAddressMap, ValueLabelsRanges,
|
||||
};
|
||||
use crate::cache::{ModuleCacheData, ModuleCacheEntry};
|
||||
use crate::compilation::{
|
||||
Compilation, CompileError, CompiledFunction, Relocation, RelocationTarget, Relocations,
|
||||
TrapInformation, Traps,
|
||||
};
|
||||
use crate::func_environ::{
|
||||
get_func_name, get_imported_memory32_grow_name, get_imported_memory32_size_name,
|
||||
get_memory32_grow_name, get_memory32_size_name, FuncEnvironment,
|
||||
};
|
||||
use crate::module::Module;
|
||||
use crate::module_environ::FunctionBodyData;
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_codegen::binemit;
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_codegen::ir::ExternalName;
|
||||
use cranelift_codegen::isa;
|
||||
use cranelift_codegen::Context;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::{DefinedFuncIndex, FuncIndex, FuncTranslator, ModuleTranslationState};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
|
||||
/// Implementation of a relocation sink that just saves all the information for later
|
||||
pub struct RelocSink {
|
||||
/// Current function index.
|
||||
func_index: FuncIndex,
|
||||
|
||||
/// Relocations recorded for the function.
|
||||
pub func_relocs: Vec<Relocation>,
|
||||
}
|
||||
|
||||
impl binemit::RelocSink for RelocSink {
|
||||
fn reloc_ebb(
|
||||
&mut self,
|
||||
_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_ebb_offset: binemit::CodeOffset,
|
||||
) {
|
||||
// This should use the `offsets` field of `ir::Function`.
|
||||
panic!("ebb headers not yet implemented");
|
||||
}
|
||||
fn reloc_external(
|
||||
&mut self,
|
||||
offset: binemit::CodeOffset,
|
||||
reloc: binemit::Reloc,
|
||||
name: &ExternalName,
|
||||
addend: binemit::Addend,
|
||||
) {
|
||||
let reloc_target = if *name == get_memory32_grow_name() {
|
||||
RelocationTarget::Memory32Grow
|
||||
} else if *name == get_imported_memory32_grow_name() {
|
||||
RelocationTarget::ImportedMemory32Grow
|
||||
} else if *name == get_memory32_size_name() {
|
||||
RelocationTarget::Memory32Size
|
||||
} else if *name == get_imported_memory32_size_name() {
|
||||
RelocationTarget::ImportedMemory32Size
|
||||
} else if let ExternalName::User { namespace, index } = *name {
|
||||
debug_assert!(namespace == 0);
|
||||
RelocationTarget::UserFunc(FuncIndex::from_u32(index))
|
||||
} else if let ExternalName::LibCall(libcall) = *name {
|
||||
RelocationTarget::LibCall(libcall)
|
||||
} else {
|
||||
panic!("unrecognized external name")
|
||||
};
|
||||
self.func_relocs.push(Relocation {
|
||||
reloc,
|
||||
reloc_target,
|
||||
offset,
|
||||
addend,
|
||||
});
|
||||
}
|
||||
|
||||
fn reloc_constant(
|
||||
&mut self,
|
||||
_code_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_constant_offset: ir::ConstantOffset,
|
||||
) {
|
||||
// Do nothing for now: cranelift emits constant data after the function code and also emits
|
||||
// function code with correct relative offsets to the constant data.
|
||||
}
|
||||
|
||||
fn reloc_jt(&mut self, offset: binemit::CodeOffset, reloc: binemit::Reloc, jt: ir::JumpTable) {
|
||||
self.func_relocs.push(Relocation {
|
||||
reloc,
|
||||
reloc_target: RelocationTarget::JumpTable(self.func_index, jt),
|
||||
offset,
|
||||
addend: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl RelocSink {
|
||||
/// Return a new `RelocSink` instance.
|
||||
pub fn new(func_index: FuncIndex) -> Self {
|
||||
Self {
|
||||
func_index,
|
||||
func_relocs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TrapSink {
|
||||
pub traps: Vec<TrapInformation>,
|
||||
}
|
||||
|
||||
impl TrapSink {
|
||||
fn new() -> Self {
|
||||
Self { traps: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl binemit::TrapSink for TrapSink {
|
||||
fn trap(
|
||||
&mut self,
|
||||
code_offset: binemit::CodeOffset,
|
||||
source_loc: ir::SourceLoc,
|
||||
trap_code: ir::TrapCode,
|
||||
) {
|
||||
self.traps.push(TrapInformation {
|
||||
code_offset,
|
||||
source_loc,
|
||||
trap_code,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn get_function_address_map<'data>(
|
||||
context: &Context,
|
||||
data: &FunctionBodyData<'data>,
|
||||
body_len: usize,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
) -> FunctionAddressMap {
|
||||
let mut instructions = Vec::new();
|
||||
|
||||
let func = &context.func;
|
||||
let mut ebbs = func.layout.ebbs().collect::<Vec<_>>();
|
||||
ebbs.sort_by_key(|ebb| func.offsets[*ebb]); // Ensure inst offsets always increase
|
||||
|
||||
let encinfo = isa.encoding_info();
|
||||
for ebb in ebbs {
|
||||
for (offset, inst, size) in func.inst_offsets(ebb, &encinfo) {
|
||||
let srcloc = func.srclocs[inst];
|
||||
instructions.push(InstructionAddressMap {
|
||||
srcloc,
|
||||
code_offset: offset as usize,
|
||||
code_len: size as usize,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate artificial srcloc for function start/end to identify boundary
|
||||
// within module. Similar to FuncTranslator::cur_srcloc(): it will wrap around
|
||||
// if byte code is larger than 4 GB.
|
||||
let start_srcloc = ir::SourceLoc::new(data.module_offset as u32);
|
||||
let end_srcloc = ir::SourceLoc::new((data.module_offset + data.data.len()) as u32);
|
||||
|
||||
FunctionAddressMap {
|
||||
instructions,
|
||||
start_srcloc,
|
||||
end_srcloc,
|
||||
body_offset: 0,
|
||||
body_len,
|
||||
}
|
||||
}
|
||||
|
||||
/// A compiler that compiles a WebAssembly module with Cranelift, translating the Wasm to Cranelift IR,
|
||||
/// optimizing it and then translating to assembly.
|
||||
pub struct Cranelift;
|
||||
|
||||
impl crate::compilation::Compiler for Cranelift {
|
||||
/// Compile the module using Cranelift, producing a compilation result with
|
||||
/// associated relocations.
|
||||
fn compile_module<'data, 'module>(
|
||||
module: &'module Module,
|
||||
module_translation: &ModuleTranslationState,
|
||||
function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
generate_debug_info: bool,
|
||||
) -> Result<
|
||||
(
|
||||
Compilation,
|
||||
Relocations,
|
||||
ModuleAddressMap,
|
||||
ValueLabelsRanges,
|
||||
PrimaryMap<DefinedFuncIndex, ir::StackSlots>,
|
||||
Traps,
|
||||
),
|
||||
CompileError,
|
||||
> {
|
||||
let cache_entry = ModuleCacheEntry::new(
|
||||
module,
|
||||
&function_body_inputs,
|
||||
isa,
|
||||
"cranelift",
|
||||
generate_debug_info,
|
||||
);
|
||||
|
||||
let data = match cache_entry.get_data() {
|
||||
Some(data) => data,
|
||||
None => {
|
||||
let mut functions = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
let mut relocations = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
let mut address_transforms = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
let mut value_ranges = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
let mut stack_slots = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
let mut traps = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
|
||||
function_body_inputs
|
||||
.into_iter()
|
||||
.collect::<Vec<(DefinedFuncIndex, &FunctionBodyData<'data>)>>()
|
||||
.par_iter()
|
||||
.map_init(
|
||||
|| FuncTranslator::new(),
|
||||
|func_translator, (i, input)| {
|
||||
let func_index = module.func_index(*i);
|
||||
let mut context = Context::new();
|
||||
context.func.name = get_func_name(func_index);
|
||||
context.func.signature =
|
||||
module.signatures[module.functions[func_index]].clone();
|
||||
if generate_debug_info {
|
||||
context.func.collect_debug_info();
|
||||
}
|
||||
|
||||
func_translator.translate(
|
||||
module_translation,
|
||||
input.data,
|
||||
input.module_offset,
|
||||
&mut context.func,
|
||||
&mut FuncEnvironment::new(isa.frontend_config(), module),
|
||||
)?;
|
||||
|
||||
let mut code_buf: Vec<u8> = Vec::new();
|
||||
let mut unwind_info = Vec::new();
|
||||
let mut reloc_sink = RelocSink::new(func_index);
|
||||
let mut trap_sink = TrapSink::new();
|
||||
let mut stackmap_sink = binemit::NullStackmapSink {};
|
||||
context.compile_and_emit(
|
||||
isa,
|
||||
&mut code_buf,
|
||||
&mut reloc_sink,
|
||||
&mut trap_sink,
|
||||
&mut stackmap_sink,
|
||||
)?;
|
||||
|
||||
context.emit_unwind_info(isa, &mut unwind_info);
|
||||
|
||||
let address_transform = if generate_debug_info {
|
||||
let body_len = code_buf.len();
|
||||
Some(get_function_address_map(&context, input, body_len, isa))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ranges = if generate_debug_info {
|
||||
Some(context.build_value_labels_ranges(isa)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((
|
||||
code_buf,
|
||||
context.func.jt_offsets,
|
||||
reloc_sink.func_relocs,
|
||||
address_transform,
|
||||
ranges,
|
||||
context.func.stack_slots,
|
||||
trap_sink.traps,
|
||||
unwind_info,
|
||||
))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, CompileError>>()?
|
||||
.into_iter()
|
||||
.for_each(
|
||||
|(
|
||||
function,
|
||||
func_jt_offsets,
|
||||
relocs,
|
||||
address_transform,
|
||||
ranges,
|
||||
sss,
|
||||
function_traps,
|
||||
unwind_info,
|
||||
)| {
|
||||
functions.push(CompiledFunction {
|
||||
body: function,
|
||||
jt_offsets: func_jt_offsets,
|
||||
unwind_info,
|
||||
});
|
||||
relocations.push(relocs);
|
||||
if let Some(address_transform) = address_transform {
|
||||
address_transforms.push(address_transform);
|
||||
}
|
||||
value_ranges.push(ranges.unwrap_or_default());
|
||||
stack_slots.push(sss);
|
||||
traps.push(function_traps);
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: Reorganize where we create the Vec for the resolved imports.
|
||||
|
||||
let data = ModuleCacheData::from_tuple((
|
||||
Compilation::new(functions),
|
||||
relocations,
|
||||
address_transforms,
|
||||
value_ranges,
|
||||
stack_slots,
|
||||
traps,
|
||||
));
|
||||
cache_entry.update_data(&data);
|
||||
data
|
||||
}
|
||||
};
|
||||
|
||||
Ok(data.to_tuple())
|
||||
}
|
||||
}
|
||||
707
crates/environ/src/func_environ.rs
Normal file
707
crates/environ/src/func_environ.rs
Normal file
@@ -0,0 +1,707 @@
|
||||
use crate::module::{MemoryPlan, MemoryStyle, Module, TableStyle};
|
||||
use crate::vmoffsets::VMOffsets;
|
||||
use crate::WASM_PAGE_SIZE;
|
||||
use alloc::vec::Vec;
|
||||
use core::clone::Clone;
|
||||
use core::convert::TryFrom;
|
||||
use cranelift_codegen::cursor::FuncCursor;
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_codegen::ir::condcodes::*;
|
||||
use cranelift_codegen::ir::immediates::{Offset32, Uimm64};
|
||||
use cranelift_codegen::ir::types::*;
|
||||
use cranelift_codegen::ir::{AbiParam, ArgumentPurpose, Function, InstBuilder, Signature};
|
||||
use cranelift_codegen::isa::TargetFrontendConfig;
|
||||
use cranelift_entity::EntityRef;
|
||||
use cranelift_wasm::{
|
||||
self, FuncIndex, GlobalIndex, GlobalVariable, MemoryIndex, SignatureIndex, TableIndex,
|
||||
WasmResult,
|
||||
};
|
||||
#[cfg(feature = "lightbeam")]
|
||||
use cranelift_wasm::{DefinedFuncIndex, DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex};
|
||||
|
||||
/// Compute an `ir::ExternalName` for a given wasm function index.
|
||||
pub fn get_func_name(func_index: FuncIndex) -> ir::ExternalName {
|
||||
ir::ExternalName::user(0, func_index.as_u32())
|
||||
}
|
||||
|
||||
/// Compute an `ir::ExternalName` for the `memory.grow` libcall for
|
||||
/// 32-bit locally-defined memories.
|
||||
pub fn get_memory32_grow_name() -> ir::ExternalName {
|
||||
ir::ExternalName::user(1, 0)
|
||||
}
|
||||
|
||||
/// Compute an `ir::ExternalName` for the `memory.grow` libcall for
|
||||
/// 32-bit imported memories.
|
||||
pub fn get_imported_memory32_grow_name() -> ir::ExternalName {
|
||||
ir::ExternalName::user(1, 1)
|
||||
}
|
||||
|
||||
/// Compute an `ir::ExternalName` for the `memory.size` libcall for
|
||||
/// 32-bit locally-defined memories.
|
||||
pub fn get_memory32_size_name() -> ir::ExternalName {
|
||||
ir::ExternalName::user(1, 2)
|
||||
}
|
||||
|
||||
/// Compute an `ir::ExternalName` for the `memory.size` libcall for
|
||||
/// 32-bit imported memories.
|
||||
pub fn get_imported_memory32_size_name() -> ir::ExternalName {
|
||||
ir::ExternalName::user(1, 3)
|
||||
}
|
||||
|
||||
/// An index type for builtin functions.
|
||||
pub struct BuiltinFunctionIndex(u32);
|
||||
|
||||
impl BuiltinFunctionIndex {
|
||||
/// Returns an index for wasm's `memory.grow` builtin function.
|
||||
pub const fn get_memory32_grow_index() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
/// Returns an index for wasm's imported `memory.grow` builtin function.
|
||||
pub const fn get_imported_memory32_grow_index() -> Self {
|
||||
Self(1)
|
||||
}
|
||||
/// Returns an index for wasm's `memory.size` builtin function.
|
||||
pub const fn get_memory32_size_index() -> Self {
|
||||
Self(2)
|
||||
}
|
||||
/// Returns an index for wasm's imported `memory.size` builtin function.
|
||||
pub const fn get_imported_memory32_size_index() -> Self {
|
||||
Self(3)
|
||||
}
|
||||
/// Returns the total number of builtin functions.
|
||||
pub const fn builtin_functions_total_number() -> u32 {
|
||||
4
|
||||
}
|
||||
|
||||
/// Return the index as an u32 number.
|
||||
pub const fn index(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The `FuncEnvironment` implementation for use by the `ModuleEnvironment`.
|
||||
pub struct FuncEnvironment<'module_environment> {
|
||||
/// Target-specified configuration.
|
||||
target_config: TargetFrontendConfig,
|
||||
|
||||
/// The module-level environment which this function-level environment belongs to.
|
||||
module: &'module_environment Module,
|
||||
|
||||
/// The Cranelift global holding the vmctx address.
|
||||
vmctx: Option<ir::GlobalValue>,
|
||||
|
||||
/// The external function signature for implementing wasm's `memory.size`
|
||||
/// for locally-defined 32-bit memories.
|
||||
memory32_size_sig: Option<ir::SigRef>,
|
||||
|
||||
/// The external function signature for implementing wasm's `memory.grow`
|
||||
/// for locally-defined memories.
|
||||
memory_grow_sig: Option<ir::SigRef>,
|
||||
|
||||
/// Offsets to struct fields accessed by JIT code.
|
||||
offsets: VMOffsets,
|
||||
}
|
||||
|
||||
impl<'module_environment> FuncEnvironment<'module_environment> {
|
||||
pub fn new(target_config: TargetFrontendConfig, module: &'module_environment Module) -> Self {
|
||||
Self {
|
||||
target_config,
|
||||
module,
|
||||
vmctx: None,
|
||||
memory32_size_sig: None,
|
||||
memory_grow_sig: None,
|
||||
offsets: VMOffsets::new(target_config.pointer_bytes(), module),
|
||||
}
|
||||
}
|
||||
|
||||
fn pointer_type(&self) -> ir::Type {
|
||||
self.target_config.pointer_type()
|
||||
}
|
||||
|
||||
fn vmctx(&mut self, func: &mut Function) -> ir::GlobalValue {
|
||||
self.vmctx.unwrap_or_else(|| {
|
||||
let vmctx = func.create_global_value(ir::GlobalValueData::VMContext);
|
||||
self.vmctx = Some(vmctx);
|
||||
vmctx
|
||||
})
|
||||
}
|
||||
|
||||
fn get_memory_grow_sig(&mut self, func: &mut Function) -> ir::SigRef {
|
||||
let sig = self.memory_grow_sig.unwrap_or_else(|| {
|
||||
func.import_signature(Signature {
|
||||
params: vec![
|
||||
AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext),
|
||||
AbiParam::new(I32),
|
||||
AbiParam::new(I32),
|
||||
],
|
||||
returns: vec![AbiParam::new(I32)],
|
||||
call_conv: self.target_config.default_call_conv,
|
||||
})
|
||||
});
|
||||
self.memory_grow_sig = Some(sig);
|
||||
sig
|
||||
}
|
||||
|
||||
/// Return the memory.grow function signature to call for the given index, along with the
|
||||
/// translated index value to pass to it and its index in `VMBuiltinFunctionsArray`.
|
||||
fn get_memory_grow_func(
|
||||
&mut self,
|
||||
func: &mut Function,
|
||||
index: MemoryIndex,
|
||||
) -> (ir::SigRef, usize, BuiltinFunctionIndex) {
|
||||
if self.module.is_imported_memory(index) {
|
||||
(
|
||||
self.get_memory_grow_sig(func),
|
||||
index.index(),
|
||||
BuiltinFunctionIndex::get_imported_memory32_grow_index(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
self.get_memory_grow_sig(func),
|
||||
self.module.defined_memory_index(index).unwrap().index(),
|
||||
BuiltinFunctionIndex::get_memory32_grow_index(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_memory32_size_sig(&mut self, func: &mut Function) -> ir::SigRef {
|
||||
let sig = self.memory32_size_sig.unwrap_or_else(|| {
|
||||
func.import_signature(Signature {
|
||||
params: vec![
|
||||
AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext),
|
||||
AbiParam::new(I32),
|
||||
],
|
||||
returns: vec![AbiParam::new(I32)],
|
||||
call_conv: self.target_config.default_call_conv,
|
||||
})
|
||||
});
|
||||
self.memory32_size_sig = Some(sig);
|
||||
sig
|
||||
}
|
||||
|
||||
/// Return the memory.size function signature to call for the given index, along with the
|
||||
/// translated index value to pass to it and its index in `VMBuiltinFunctionsArray`.
|
||||
fn get_memory_size_func(
|
||||
&mut self,
|
||||
func: &mut Function,
|
||||
index: MemoryIndex,
|
||||
) -> (ir::SigRef, usize, BuiltinFunctionIndex) {
|
||||
if self.module.is_imported_memory(index) {
|
||||
(
|
||||
self.get_memory32_size_sig(func),
|
||||
index.index(),
|
||||
BuiltinFunctionIndex::get_imported_memory32_size_index(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
self.get_memory32_size_sig(func),
|
||||
self.module.defined_memory_index(index).unwrap().index(),
|
||||
BuiltinFunctionIndex::get_memory32_size_index(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Translates load of builtin function and returns a pair of values `vmctx`
|
||||
/// and address of the loaded function.
|
||||
fn translate_load_builtin_function_address(
|
||||
&mut self,
|
||||
pos: &mut FuncCursor<'_>,
|
||||
callee_func_idx: BuiltinFunctionIndex,
|
||||
) -> (ir::Value, ir::Value) {
|
||||
// We use an indirect call so that we don't have to patch the code at runtime.
|
||||
let pointer_type = self.pointer_type();
|
||||
let vmctx = self.vmctx(&mut pos.func);
|
||||
let base = pos.ins().global_value(pointer_type, vmctx);
|
||||
|
||||
let mut mem_flags = ir::MemFlags::trusted();
|
||||
mem_flags.set_readonly();
|
||||
|
||||
// Load the callee address.
|
||||
let body_offset =
|
||||
i32::try_from(self.offsets.vmctx_builtin_function(callee_func_idx)).unwrap();
|
||||
let func_addr = pos.ins().load(pointer_type, mem_flags, base, body_offset);
|
||||
|
||||
(base, func_addr)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lightbeam")]
|
||||
impl lightbeam::ModuleContext for FuncEnvironment<'_> {
|
||||
type Signature = ir::Signature;
|
||||
type GlobalType = ir::Type;
|
||||
|
||||
fn func_index(&self, defined_func_index: u32) -> u32 {
|
||||
self.module
|
||||
.func_index(DefinedFuncIndex::from_u32(defined_func_index))
|
||||
.as_u32()
|
||||
}
|
||||
|
||||
fn defined_func_index(&self, func_index: u32) -> Option<u32> {
|
||||
self.module
|
||||
.defined_func_index(FuncIndex::from_u32(func_index))
|
||||
.map(DefinedFuncIndex::as_u32)
|
||||
}
|
||||
|
||||
fn defined_global_index(&self, global_index: u32) -> Option<u32> {
|
||||
self.module
|
||||
.defined_global_index(GlobalIndex::from_u32(global_index))
|
||||
.map(DefinedGlobalIndex::as_u32)
|
||||
}
|
||||
|
||||
fn global_type(&self, global_index: u32) -> &Self::GlobalType {
|
||||
&self.module.globals[GlobalIndex::from_u32(global_index)].ty
|
||||
}
|
||||
|
||||
fn func_type_index(&self, func_idx: u32) -> u32 {
|
||||
self.module.functions[FuncIndex::from_u32(func_idx)].as_u32()
|
||||
}
|
||||
|
||||
fn signature(&self, index: u32) -> &Self::Signature {
|
||||
&self.module.signatures[SignatureIndex::from_u32(index)]
|
||||
}
|
||||
|
||||
fn defined_table_index(&self, table_index: u32) -> Option<u32> {
|
||||
self.module
|
||||
.defined_table_index(TableIndex::from_u32(table_index))
|
||||
.map(DefinedTableIndex::as_u32)
|
||||
}
|
||||
|
||||
fn defined_memory_index(&self, memory_index: u32) -> Option<u32> {
|
||||
self.module
|
||||
.defined_memory_index(MemoryIndex::from_u32(memory_index))
|
||||
.map(DefinedMemoryIndex::as_u32)
|
||||
}
|
||||
|
||||
fn vmctx_vmfunction_import_body(&self, func_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmfunction_import_body(FuncIndex::from_u32(func_index))
|
||||
}
|
||||
fn vmctx_vmfunction_import_vmctx(&self, func_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmfunction_import_vmctx(FuncIndex::from_u32(func_index))
|
||||
}
|
||||
|
||||
fn vmctx_vmglobal_import_from(&self, global_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmglobal_import_from(GlobalIndex::from_u32(global_index))
|
||||
}
|
||||
fn vmctx_vmglobal_definition(&self, defined_global_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmglobal_definition(DefinedGlobalIndex::from_u32(defined_global_index))
|
||||
}
|
||||
fn vmctx_vmmemory_import_from(&self, memory_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmmemory_import_from(MemoryIndex::from_u32(memory_index))
|
||||
}
|
||||
fn vmctx_vmmemory_definition(&self, defined_memory_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmmemory_definition(DefinedMemoryIndex::from_u32(defined_memory_index))
|
||||
}
|
||||
fn vmctx_vmmemory_definition_base(&self, defined_memory_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmmemory_definition_base(DefinedMemoryIndex::from_u32(defined_memory_index))
|
||||
}
|
||||
fn vmctx_vmmemory_definition_current_length(&self, defined_memory_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmmemory_definition_current_length(DefinedMemoryIndex::from_u32(
|
||||
defined_memory_index,
|
||||
))
|
||||
}
|
||||
fn vmmemory_definition_base(&self) -> u8 {
|
||||
self.offsets.vmmemory_definition_base()
|
||||
}
|
||||
fn vmmemory_definition_current_length(&self) -> u8 {
|
||||
self.offsets.vmmemory_definition_current_length()
|
||||
}
|
||||
fn vmctx_vmtable_import_from(&self, table_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmtable_import_from(TableIndex::from_u32(table_index))
|
||||
}
|
||||
fn vmctx_vmtable_definition(&self, defined_table_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmtable_definition(DefinedTableIndex::from_u32(defined_table_index))
|
||||
}
|
||||
fn vmctx_vmtable_definition_base(&self, defined_table_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmtable_definition_base(DefinedTableIndex::from_u32(defined_table_index))
|
||||
}
|
||||
fn vmctx_vmtable_definition_current_elements(&self, defined_table_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmtable_definition_current_elements(DefinedTableIndex::from_u32(
|
||||
defined_table_index,
|
||||
))
|
||||
}
|
||||
fn vmtable_definition_base(&self) -> u8 {
|
||||
self.offsets.vmtable_definition_base()
|
||||
}
|
||||
fn vmtable_definition_current_elements(&self) -> u8 {
|
||||
self.offsets.vmtable_definition_current_elements()
|
||||
}
|
||||
fn vmcaller_checked_anyfunc_type_index(&self) -> u8 {
|
||||
self.offsets.vmcaller_checked_anyfunc_type_index()
|
||||
}
|
||||
fn vmcaller_checked_anyfunc_func_ptr(&self) -> u8 {
|
||||
self.offsets.vmcaller_checked_anyfunc_func_ptr()
|
||||
}
|
||||
fn vmcaller_checked_anyfunc_vmctx(&self) -> u8 {
|
||||
self.offsets.vmcaller_checked_anyfunc_vmctx()
|
||||
}
|
||||
fn size_of_vmcaller_checked_anyfunc(&self) -> u8 {
|
||||
self.offsets.size_of_vmcaller_checked_anyfunc()
|
||||
}
|
||||
fn vmctx_vmshared_signature_id(&self, signature_idx: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmshared_signature_id(SignatureIndex::from_u32(signature_idx))
|
||||
}
|
||||
|
||||
// TODO: type of a global
|
||||
}
|
||||
|
||||
impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'module_environment> {
|
||||
fn target_config(&self) -> TargetFrontendConfig {
|
||||
self.target_config
|
||||
}
|
||||
|
||||
fn make_table(&mut self, func: &mut ir::Function, index: TableIndex) -> WasmResult<ir::Table> {
|
||||
let pointer_type = self.pointer_type();
|
||||
|
||||
let (ptr, base_offset, current_elements_offset) = {
|
||||
let vmctx = self.vmctx(func);
|
||||
if let Some(def_index) = self.module.defined_table_index(index) {
|
||||
let base_offset =
|
||||
i32::try_from(self.offsets.vmctx_vmtable_definition_base(def_index)).unwrap();
|
||||
let current_elements_offset = i32::try_from(
|
||||
self.offsets
|
||||
.vmctx_vmtable_definition_current_elements(def_index),
|
||||
)
|
||||
.unwrap();
|
||||
(vmctx, base_offset, current_elements_offset)
|
||||
} else {
|
||||
let from_offset = self.offsets.vmctx_vmtable_import_from(index);
|
||||
let table = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: vmctx,
|
||||
offset: Offset32::new(i32::try_from(from_offset).unwrap()),
|
||||
global_type: pointer_type,
|
||||
readonly: true,
|
||||
});
|
||||
let base_offset = i32::from(self.offsets.vmtable_definition_base());
|
||||
let current_elements_offset =
|
||||
i32::from(self.offsets.vmtable_definition_current_elements());
|
||||
(table, base_offset, current_elements_offset)
|
||||
}
|
||||
};
|
||||
|
||||
let base_gv = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: ptr,
|
||||
offset: Offset32::new(base_offset),
|
||||
global_type: pointer_type,
|
||||
readonly: false,
|
||||
});
|
||||
let bound_gv = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: ptr,
|
||||
offset: Offset32::new(current_elements_offset),
|
||||
global_type: self.offsets.type_of_vmtable_definition_current_elements(),
|
||||
readonly: false,
|
||||
});
|
||||
|
||||
let element_size = match self.module.table_plans[index].style {
|
||||
TableStyle::CallerChecksSignature => {
|
||||
u64::from(self.offsets.size_of_vmcaller_checked_anyfunc())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(func.create_table(ir::TableData {
|
||||
base_gv,
|
||||
min_size: Uimm64::new(0),
|
||||
bound_gv,
|
||||
element_size: Uimm64::new(element_size),
|
||||
index_type: I32,
|
||||
}))
|
||||
}
|
||||
|
||||
fn make_heap(&mut self, func: &mut ir::Function, index: MemoryIndex) -> WasmResult<ir::Heap> {
|
||||
let pointer_type = self.pointer_type();
|
||||
|
||||
let (ptr, base_offset, current_length_offset) = {
|
||||
let vmctx = self.vmctx(func);
|
||||
if let Some(def_index) = self.module.defined_memory_index(index) {
|
||||
let base_offset =
|
||||
i32::try_from(self.offsets.vmctx_vmmemory_definition_base(def_index)).unwrap();
|
||||
let current_length_offset = i32::try_from(
|
||||
self.offsets
|
||||
.vmctx_vmmemory_definition_current_length(def_index),
|
||||
)
|
||||
.unwrap();
|
||||
(vmctx, base_offset, current_length_offset)
|
||||
} else {
|
||||
let from_offset = self.offsets.vmctx_vmmemory_import_from(index);
|
||||
let memory = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: vmctx,
|
||||
offset: Offset32::new(i32::try_from(from_offset).unwrap()),
|
||||
global_type: pointer_type,
|
||||
readonly: true,
|
||||
});
|
||||
let base_offset = i32::from(self.offsets.vmmemory_definition_base());
|
||||
let current_length_offset =
|
||||
i32::from(self.offsets.vmmemory_definition_current_length());
|
||||
(memory, base_offset, current_length_offset)
|
||||
}
|
||||
};
|
||||
|
||||
// If we have a declared maximum, we can make this a "static" heap, which is
|
||||
// allocated up front and never moved.
|
||||
let (offset_guard_size, heap_style, readonly_base) = match self.module.memory_plans[index] {
|
||||
MemoryPlan {
|
||||
memory: _,
|
||||
style: MemoryStyle::Dynamic,
|
||||
offset_guard_size,
|
||||
} => {
|
||||
let heap_bound = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: ptr,
|
||||
offset: Offset32::new(current_length_offset),
|
||||
global_type: self.offsets.type_of_vmmemory_definition_current_length(),
|
||||
readonly: false,
|
||||
});
|
||||
(
|
||||
Uimm64::new(offset_guard_size),
|
||||
ir::HeapStyle::Dynamic {
|
||||
bound_gv: heap_bound,
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
MemoryPlan {
|
||||
memory: _,
|
||||
style: MemoryStyle::Static { bound },
|
||||
offset_guard_size,
|
||||
} => (
|
||||
Uimm64::new(offset_guard_size),
|
||||
ir::HeapStyle::Static {
|
||||
bound: Uimm64::new(u64::from(bound) * u64::from(WASM_PAGE_SIZE)),
|
||||
},
|
||||
true,
|
||||
),
|
||||
};
|
||||
|
||||
let heap_base = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: ptr,
|
||||
offset: Offset32::new(base_offset),
|
||||
global_type: pointer_type,
|
||||
readonly: readonly_base,
|
||||
});
|
||||
Ok(func.create_heap(ir::HeapData {
|
||||
base: heap_base,
|
||||
min_size: 0.into(),
|
||||
offset_guard_size,
|
||||
style: heap_style,
|
||||
index_type: I32,
|
||||
}))
|
||||
}
|
||||
|
||||
fn make_global(
|
||||
&mut self,
|
||||
func: &mut ir::Function,
|
||||
index: GlobalIndex,
|
||||
) -> WasmResult<GlobalVariable> {
|
||||
let pointer_type = self.pointer_type();
|
||||
|
||||
let (ptr, offset) = {
|
||||
let vmctx = self.vmctx(func);
|
||||
if let Some(def_index) = self.module.defined_global_index(index) {
|
||||
let offset =
|
||||
i32::try_from(self.offsets.vmctx_vmglobal_definition(def_index)).unwrap();
|
||||
(vmctx, offset)
|
||||
} else {
|
||||
let from_offset = self.offsets.vmctx_vmglobal_import_from(index);
|
||||
let global = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: vmctx,
|
||||
offset: Offset32::new(i32::try_from(from_offset).unwrap()),
|
||||
global_type: pointer_type,
|
||||
readonly: true,
|
||||
});
|
||||
(global, 0)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GlobalVariable::Memory {
|
||||
gv: ptr,
|
||||
offset: offset.into(),
|
||||
ty: self.module.globals[index].ty,
|
||||
})
|
||||
}
|
||||
|
||||
fn make_indirect_sig(
|
||||
&mut self,
|
||||
func: &mut ir::Function,
|
||||
index: SignatureIndex,
|
||||
) -> WasmResult<ir::SigRef> {
|
||||
Ok(func.import_signature(self.module.signatures[index].clone()))
|
||||
}
|
||||
|
||||
fn make_direct_func(
|
||||
&mut self,
|
||||
func: &mut ir::Function,
|
||||
index: FuncIndex,
|
||||
) -> WasmResult<ir::FuncRef> {
|
||||
let sigidx = self.module.functions[index];
|
||||
let signature = func.import_signature(self.module.signatures[sigidx].clone());
|
||||
let name = get_func_name(index);
|
||||
Ok(func.import_function(ir::ExtFuncData {
|
||||
name,
|
||||
signature,
|
||||
// We currently allocate all code segments independently, so nothing
|
||||
// is colocated.
|
||||
colocated: false,
|
||||
}))
|
||||
}
|
||||
|
||||
fn translate_call_indirect(
|
||||
&mut self,
|
||||
mut pos: FuncCursor<'_>,
|
||||
table_index: TableIndex,
|
||||
table: ir::Table,
|
||||
sig_index: SignatureIndex,
|
||||
sig_ref: ir::SigRef,
|
||||
callee: ir::Value,
|
||||
call_args: &[ir::Value],
|
||||
) -> WasmResult<ir::Inst> {
|
||||
let pointer_type = self.pointer_type();
|
||||
|
||||
let table_entry_addr = pos.ins().table_addr(pointer_type, table, callee, 0);
|
||||
|
||||
// Dereference table_entry_addr to get the function address.
|
||||
let mem_flags = ir::MemFlags::trusted();
|
||||
let func_addr = pos.ins().load(
|
||||
pointer_type,
|
||||
mem_flags,
|
||||
table_entry_addr,
|
||||
i32::from(self.offsets.vmcaller_checked_anyfunc_func_ptr()),
|
||||
);
|
||||
|
||||
// Check whether `func_addr` is null.
|
||||
pos.ins().trapz(func_addr, ir::TrapCode::IndirectCallToNull);
|
||||
|
||||
// If necessary, check the signature.
|
||||
match self.module.table_plans[table_index].style {
|
||||
TableStyle::CallerChecksSignature => {
|
||||
let sig_id_size = self.offsets.size_of_vmshared_signature_index();
|
||||
let sig_id_type = Type::int(u16::from(sig_id_size) * 8).unwrap();
|
||||
let vmctx = self.vmctx(pos.func);
|
||||
let base = pos.ins().global_value(pointer_type, vmctx);
|
||||
let offset =
|
||||
i32::try_from(self.offsets.vmctx_vmshared_signature_id(sig_index)).unwrap();
|
||||
|
||||
// Load the caller ID.
|
||||
let mut mem_flags = ir::MemFlags::trusted();
|
||||
mem_flags.set_readonly();
|
||||
let caller_sig_id = pos.ins().load(sig_id_type, mem_flags, base, offset);
|
||||
|
||||
// Load the callee ID.
|
||||
let mem_flags = ir::MemFlags::trusted();
|
||||
let callee_sig_id = pos.ins().load(
|
||||
sig_id_type,
|
||||
mem_flags,
|
||||
table_entry_addr,
|
||||
i32::from(self.offsets.vmcaller_checked_anyfunc_type_index()),
|
||||
);
|
||||
|
||||
// Check that they match.
|
||||
let cmp = pos.ins().icmp(IntCC::Equal, callee_sig_id, caller_sig_id);
|
||||
pos.ins().trapz(cmp, ir::TrapCode::BadSignature);
|
||||
}
|
||||
}
|
||||
|
||||
let mut real_call_args = Vec::with_capacity(call_args.len() + 1);
|
||||
|
||||
// First append the callee vmctx address.
|
||||
let vmctx = pos.ins().load(
|
||||
pointer_type,
|
||||
mem_flags,
|
||||
table_entry_addr,
|
||||
i32::from(self.offsets.vmcaller_checked_anyfunc_vmctx()),
|
||||
);
|
||||
real_call_args.push(vmctx);
|
||||
|
||||
// Then append the regular call arguments.
|
||||
real_call_args.extend_from_slice(call_args);
|
||||
|
||||
Ok(pos.ins().call_indirect(sig_ref, func_addr, &real_call_args))
|
||||
}
|
||||
|
||||
fn translate_call(
|
||||
&mut self,
|
||||
mut pos: FuncCursor<'_>,
|
||||
callee_index: FuncIndex,
|
||||
callee: ir::FuncRef,
|
||||
call_args: &[ir::Value],
|
||||
) -> WasmResult<ir::Inst> {
|
||||
let mut real_call_args = Vec::with_capacity(call_args.len() + 1);
|
||||
|
||||
// Handle direct calls to locally-defined functions.
|
||||
if !self.module.is_imported_function(callee_index) {
|
||||
// First append the callee vmctx address.
|
||||
real_call_args.push(pos.func.special_param(ArgumentPurpose::VMContext).unwrap());
|
||||
|
||||
// Then append the regular call arguments.
|
||||
real_call_args.extend_from_slice(call_args);
|
||||
|
||||
return Ok(pos.ins().call(callee, &real_call_args));
|
||||
}
|
||||
|
||||
// Handle direct calls to imported functions. We use an indirect call
|
||||
// so that we don't have to patch the code at runtime.
|
||||
let pointer_type = self.pointer_type();
|
||||
let sig_ref = pos.func.dfg.ext_funcs[callee].signature;
|
||||
let vmctx = self.vmctx(&mut pos.func);
|
||||
let base = pos.ins().global_value(pointer_type, vmctx);
|
||||
|
||||
let mem_flags = ir::MemFlags::trusted();
|
||||
|
||||
// Load the callee address.
|
||||
let body_offset =
|
||||
i32::try_from(self.offsets.vmctx_vmfunction_import_body(callee_index)).unwrap();
|
||||
let func_addr = pos.ins().load(pointer_type, mem_flags, base, body_offset);
|
||||
|
||||
// First append the callee vmctx address.
|
||||
let vmctx_offset =
|
||||
i32::try_from(self.offsets.vmctx_vmfunction_import_vmctx(callee_index)).unwrap();
|
||||
let vmctx = pos.ins().load(pointer_type, mem_flags, base, vmctx_offset);
|
||||
real_call_args.push(vmctx);
|
||||
|
||||
// Then append the regular call arguments.
|
||||
real_call_args.extend_from_slice(call_args);
|
||||
|
||||
Ok(pos.ins().call_indirect(sig_ref, func_addr, &real_call_args))
|
||||
}
|
||||
|
||||
fn translate_memory_grow(
|
||||
&mut self,
|
||||
mut pos: FuncCursor<'_>,
|
||||
index: MemoryIndex,
|
||||
_heap: ir::Heap,
|
||||
val: ir::Value,
|
||||
) -> WasmResult<ir::Value> {
|
||||
let (func_sig, index_arg, func_idx) = self.get_memory_grow_func(&mut pos.func, index);
|
||||
let memory_index = pos.ins().iconst(I32, index_arg as i64);
|
||||
let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx);
|
||||
let call_inst = pos
|
||||
.ins()
|
||||
.call_indirect(func_sig, func_addr, &[vmctx, val, memory_index]);
|
||||
Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
|
||||
}
|
||||
|
||||
fn translate_memory_size(
|
||||
&mut self,
|
||||
mut pos: FuncCursor<'_>,
|
||||
index: MemoryIndex,
|
||||
_heap: ir::Heap,
|
||||
) -> WasmResult<ir::Value> {
|
||||
let (func_sig, index_arg, func_idx) = self.get_memory_size_func(&mut pos.func, index);
|
||||
let memory_index = pos.ins().iconst(I32, index_arg as i64);
|
||||
let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx);
|
||||
let call_inst = pos
|
||||
.ins()
|
||||
.call_indirect(func_sig, func_addr, &[vmctx, memory_index]);
|
||||
Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
|
||||
}
|
||||
}
|
||||
73
crates/environ/src/lib.rs
Normal file
73
crates/environ/src/lib.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
//! Standalone environment for WebAssembly using Cranelift. Provides functions to translate
|
||||
//! `get_global`, `set_global`, `memory.size`, `memory.grow`, `call_indirect` that hardcode in
|
||||
//! the translation the base addresses of regions of memory that will hold the globals, tables and
|
||||
//! linear memories.
|
||||
|
||||
#![deny(missing_docs, trivial_numeric_casts, unused_extern_crates)]
|
||||
#![warn(unused_import_braces)]
|
||||
#![cfg_attr(feature = "std", deny(unstable_features))]
|
||||
#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))]
|
||||
#![cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
allow(clippy::new_without_default, clippy::new_without_default_derive)
|
||||
)]
|
||||
#![cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
warn(
|
||||
clippy::float_arithmetic,
|
||||
clippy::mut_mut,
|
||||
clippy::nonminimal_bool,
|
||||
clippy::option_map_unwrap_or,
|
||||
clippy::option_map_unwrap_or_else,
|
||||
clippy::print_stdout,
|
||||
clippy::unicode_not_nfc,
|
||||
clippy::use_self
|
||||
)
|
||||
)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
mod address_map;
|
||||
mod compilation;
|
||||
mod func_environ;
|
||||
mod module;
|
||||
mod module_environ;
|
||||
mod tunables;
|
||||
mod vmoffsets;
|
||||
|
||||
mod cache;
|
||||
|
||||
pub mod cranelift;
|
||||
#[cfg(feature = "lightbeam")]
|
||||
pub mod lightbeam;
|
||||
|
||||
pub use crate::address_map::{
|
||||
FunctionAddressMap, InstructionAddressMap, ModuleAddressMap, ModuleVmctxInfo, ValueLabelsRanges,
|
||||
};
|
||||
pub use crate::cache::{create_new_config as cache_create_new_config, init as cache_init};
|
||||
pub use crate::compilation::{
|
||||
Compilation, CompileError, CompiledFunction, Compiler, Relocation, RelocationTarget,
|
||||
Relocations, TrapInformation, Traps,
|
||||
};
|
||||
pub use crate::cranelift::Cranelift;
|
||||
pub use crate::func_environ::BuiltinFunctionIndex;
|
||||
#[cfg(feature = "lightbeam")]
|
||||
pub use crate::lightbeam::Lightbeam;
|
||||
pub use crate::module::{
|
||||
Export, MemoryPlan, MemoryStyle, Module, TableElements, TablePlan, TableStyle,
|
||||
};
|
||||
pub use crate::module_environ::{
|
||||
translate_signature, DataInitializer, DataInitializerLocation, FunctionBodyData,
|
||||
ModuleEnvironment, ModuleTranslation,
|
||||
};
|
||||
pub use crate::tunables::Tunables;
|
||||
pub use crate::vmoffsets::{TargetSharedSignatureIndex, VMOffsets};
|
||||
|
||||
/// WebAssembly page sizes are defined to be 64KiB.
|
||||
pub const WASM_PAGE_SIZE: u32 = 0x10000;
|
||||
|
||||
/// The number of pages we can have before we run out of byte index space.
|
||||
pub const WASM_MAX_PAGES: u32 = 0x10000;
|
||||
|
||||
/// Version number of this crate.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
83
crates/environ/src/lightbeam.rs
Normal file
83
crates/environ/src/lightbeam.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! Support for compiling with Lightbeam.
|
||||
|
||||
use crate::compilation::{Compilation, CompileError, Relocations, Traps};
|
||||
use crate::func_environ::FuncEnvironment;
|
||||
use crate::module::Module;
|
||||
use crate::module_environ::FunctionBodyData;
|
||||
// TODO: Put this in `compilation`
|
||||
use crate::address_map::{ModuleAddressMap, ValueLabelsRanges};
|
||||
use crate::cranelift::RelocSink;
|
||||
use cranelift_codegen::{ir, isa};
|
||||
use cranelift_entity::{PrimaryMap, SecondaryMap};
|
||||
use cranelift_wasm::{DefinedFuncIndex, ModuleTranslationState};
|
||||
use lightbeam;
|
||||
|
||||
/// A compiler that compiles a WebAssembly module with Lightbeam, directly translating the Wasm file.
|
||||
pub struct Lightbeam;
|
||||
|
||||
impl crate::compilation::Compiler for Lightbeam {
|
||||
/// Compile the module using Lightbeam, producing a compilation result with
|
||||
/// associated relocations.
|
||||
fn compile_module<'data, 'module>(
|
||||
module: &'module Module,
|
||||
_module_translation: &ModuleTranslationState,
|
||||
function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
// TODO
|
||||
generate_debug_info: bool,
|
||||
) -> Result<
|
||||
(
|
||||
Compilation,
|
||||
Relocations,
|
||||
ModuleAddressMap,
|
||||
ValueLabelsRanges,
|
||||
PrimaryMap<DefinedFuncIndex, ir::StackSlots>,
|
||||
Traps,
|
||||
),
|
||||
CompileError,
|
||||
> {
|
||||
if generate_debug_info {
|
||||
return Err(CompileError::DebugInfoNotSupported);
|
||||
}
|
||||
|
||||
let env = FuncEnvironment::new(isa.frontend_config(), module);
|
||||
let mut relocations = PrimaryMap::new();
|
||||
let mut codegen_session: lightbeam::CodeGenSession<_> =
|
||||
lightbeam::CodeGenSession::new(function_body_inputs.len() as u32, &env);
|
||||
|
||||
for (i, function_body) in &function_body_inputs {
|
||||
let func_index = module.func_index(i);
|
||||
let mut reloc_sink = RelocSink::new(func_index);
|
||||
|
||||
lightbeam::translate_function(
|
||||
&mut codegen_session,
|
||||
&mut reloc_sink,
|
||||
i.as_u32(),
|
||||
&lightbeam::wasmparser::FunctionBody::new(0, function_body.data),
|
||||
)
|
||||
.expect("Failed to translate function. TODO: Stop this from panicking");
|
||||
relocations.push(reloc_sink.func_relocs);
|
||||
}
|
||||
|
||||
let code_section = codegen_session
|
||||
.into_translated_code_section()
|
||||
.expect("Failed to generate output code. TODO: Stop this from panicking");
|
||||
|
||||
// TODO pass jump table offsets to Compilation::from_buffer() when they
|
||||
// are implemented in lightbeam -- using empty set of offsets for now.
|
||||
// TODO: pass an empty range for the unwind information until lightbeam emits it
|
||||
let code_section_ranges_and_jt = code_section
|
||||
.funcs()
|
||||
.into_iter()
|
||||
.map(|r| (r, SecondaryMap::new(), 0..0));
|
||||
|
||||
Ok((
|
||||
Compilation::from_buffer(code_section.buffer().to_vec(), code_section_ranges_and_jt),
|
||||
relocations,
|
||||
ModuleAddressMap::new(),
|
||||
ValueLabelsRanges::new(),
|
||||
PrimaryMap::new(),
|
||||
Traps::new(),
|
||||
))
|
||||
}
|
||||
}
|
||||
306
crates/environ/src/module.rs
Normal file
306
crates/environ/src/module.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
//! Data structures for representing decoded wasm modules.
|
||||
|
||||
use crate::module_environ::FunctionBodyData;
|
||||
use crate::tunables::Tunables;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_entity::{EntityRef, PrimaryMap};
|
||||
use cranelift_wasm::{
|
||||
DefinedFuncIndex, DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, FuncIndex, Global,
|
||||
GlobalIndex, Memory, MemoryIndex, SignatureIndex, Table, TableIndex,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
|
||||
/// A WebAssembly table initializer.
|
||||
#[derive(Clone, Debug, Hash)]
|
||||
pub struct TableElements {
|
||||
/// The index of a table to initialize.
|
||||
pub table_index: TableIndex,
|
||||
/// Optionally, a global variable giving a base index.
|
||||
pub base: Option<GlobalIndex>,
|
||||
/// The offset to add to the base.
|
||||
pub offset: usize,
|
||||
/// The values to write into the table elements.
|
||||
pub elements: Box<[FuncIndex]>,
|
||||
}
|
||||
|
||||
/// An entity to export.
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Export {
|
||||
/// Function export.
|
||||
Function(FuncIndex),
|
||||
/// Table export.
|
||||
Table(TableIndex),
|
||||
/// Memory export.
|
||||
Memory(MemoryIndex),
|
||||
/// Global export.
|
||||
Global(GlobalIndex),
|
||||
}
|
||||
|
||||
/// Implemenation styles for WebAssembly linear memory.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum MemoryStyle {
|
||||
/// The actual memory can be resized and moved.
|
||||
Dynamic,
|
||||
/// Addresss space is allocated up front.
|
||||
Static {
|
||||
/// The number of mapped and unmapped pages.
|
||||
bound: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl MemoryStyle {
|
||||
/// Decide on an implementation style for the given `Memory`.
|
||||
pub fn for_memory(memory: Memory, tunables: &Tunables) -> (Self, u64) {
|
||||
if let Some(maximum) = memory.maximum {
|
||||
if maximum <= tunables.static_memory_bound {
|
||||
// A heap with a declared maximum can be immovable, so make
|
||||
// it static.
|
||||
assert!(tunables.static_memory_bound >= memory.minimum);
|
||||
return (
|
||||
Self::Static {
|
||||
bound: tunables.static_memory_bound,
|
||||
},
|
||||
tunables.static_memory_offset_guard_size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, make it dynamic.
|
||||
(Self::Dynamic, tunables.dynamic_memory_offset_guard_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// A WebAssembly linear memory description along with our chosen style for
|
||||
/// implementing it.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct MemoryPlan {
|
||||
/// The WebAssembly linear memory description.
|
||||
pub memory: Memory,
|
||||
/// Our chosen implementation style.
|
||||
pub style: MemoryStyle,
|
||||
/// Our chosen offset-guard size.
|
||||
pub offset_guard_size: u64,
|
||||
}
|
||||
|
||||
impl MemoryPlan {
|
||||
/// Draw up a plan for implementing a `Memory`.
|
||||
pub fn for_memory(memory: Memory, tunables: &Tunables) -> Self {
|
||||
let (style, offset_guard_size) = MemoryStyle::for_memory(memory, tunables);
|
||||
Self {
|
||||
memory,
|
||||
style,
|
||||
offset_guard_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implemenation styles for WebAssembly tables.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum TableStyle {
|
||||
/// Signatures are stored in the table and checked in the caller.
|
||||
CallerChecksSignature,
|
||||
}
|
||||
|
||||
impl TableStyle {
|
||||
/// Decide on an implementation style for the given `Table`.
|
||||
pub fn for_table(_table: Table, _tunables: &Tunables) -> Self {
|
||||
Self::CallerChecksSignature
|
||||
}
|
||||
}
|
||||
|
||||
/// A WebAssembly table description along with our chosen style for
|
||||
/// implementing it.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct TablePlan {
|
||||
/// The WebAssembly table description.
|
||||
pub table: cranelift_wasm::Table,
|
||||
/// Our chosen implementation style.
|
||||
pub style: TableStyle,
|
||||
}
|
||||
|
||||
impl TablePlan {
|
||||
/// Draw up a plan for implementing a `Table`.
|
||||
pub fn for_table(table: Table, tunables: &Tunables) -> Self {
|
||||
let style = TableStyle::for_table(table, tunables);
|
||||
Self { table, style }
|
||||
}
|
||||
}
|
||||
|
||||
/// A translated WebAssembly module, excluding the function bodies and
|
||||
/// memory initializers.
|
||||
// WARNING: when modifying, make sure that `hash_for_cache` is still valid!
|
||||
#[derive(Debug)]
|
||||
pub struct Module {
|
||||
/// Unprocessed signatures exactly as provided by `declare_signature()`.
|
||||
pub signatures: PrimaryMap<SignatureIndex, ir::Signature>,
|
||||
|
||||
/// Names of imported functions.
|
||||
pub imported_funcs: PrimaryMap<FuncIndex, (String, String)>,
|
||||
|
||||
/// Names of imported tables.
|
||||
pub imported_tables: PrimaryMap<TableIndex, (String, String)>,
|
||||
|
||||
/// Names of imported memories.
|
||||
pub imported_memories: PrimaryMap<MemoryIndex, (String, String)>,
|
||||
|
||||
/// Names of imported globals.
|
||||
pub imported_globals: PrimaryMap<GlobalIndex, (String, String)>,
|
||||
|
||||
/// Types of functions, imported and local.
|
||||
pub functions: PrimaryMap<FuncIndex, SignatureIndex>,
|
||||
|
||||
/// WebAssembly tables.
|
||||
pub table_plans: PrimaryMap<TableIndex, TablePlan>,
|
||||
|
||||
/// WebAssembly linear memory plans.
|
||||
pub memory_plans: PrimaryMap<MemoryIndex, MemoryPlan>,
|
||||
|
||||
/// WebAssembly global variables.
|
||||
pub globals: PrimaryMap<GlobalIndex, Global>,
|
||||
|
||||
/// Exported entities.
|
||||
pub exports: IndexMap<String, Export>,
|
||||
|
||||
/// The module "start" function, if present.
|
||||
pub start_func: Option<FuncIndex>,
|
||||
|
||||
/// WebAssembly table initializers.
|
||||
pub table_elements: Vec<TableElements>,
|
||||
}
|
||||
|
||||
impl Module {
|
||||
/// Allocates the module data structures.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signatures: PrimaryMap::new(),
|
||||
imported_funcs: PrimaryMap::new(),
|
||||
imported_tables: PrimaryMap::new(),
|
||||
imported_memories: PrimaryMap::new(),
|
||||
imported_globals: PrimaryMap::new(),
|
||||
functions: PrimaryMap::new(),
|
||||
table_plans: PrimaryMap::new(),
|
||||
memory_plans: PrimaryMap::new(),
|
||||
globals: PrimaryMap::new(),
|
||||
exports: IndexMap::new(),
|
||||
start_func: None,
|
||||
table_elements: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a `DefinedFuncIndex` into a `FuncIndex`.
|
||||
pub fn func_index(&self, defined_func: DefinedFuncIndex) -> FuncIndex {
|
||||
FuncIndex::new(self.imported_funcs.len() + defined_func.index())
|
||||
}
|
||||
|
||||
/// Convert a `FuncIndex` into a `DefinedFuncIndex`. Returns None if the
|
||||
/// index is an imported function.
|
||||
pub fn defined_func_index(&self, func: FuncIndex) -> Option<DefinedFuncIndex> {
|
||||
if func.index() < self.imported_funcs.len() {
|
||||
None
|
||||
} else {
|
||||
Some(DefinedFuncIndex::new(
|
||||
func.index() - self.imported_funcs.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether the given function index is for an imported function.
|
||||
pub fn is_imported_function(&self, index: FuncIndex) -> bool {
|
||||
index.index() < self.imported_funcs.len()
|
||||
}
|
||||
|
||||
/// Convert a `DefinedTableIndex` into a `TableIndex`.
|
||||
pub fn table_index(&self, defined_table: DefinedTableIndex) -> TableIndex {
|
||||
TableIndex::new(self.imported_tables.len() + defined_table.index())
|
||||
}
|
||||
|
||||
/// Convert a `TableIndex` into a `DefinedTableIndex`. Returns None if the
|
||||
/// index is an imported table.
|
||||
pub fn defined_table_index(&self, table: TableIndex) -> Option<DefinedTableIndex> {
|
||||
if table.index() < self.imported_tables.len() {
|
||||
None
|
||||
} else {
|
||||
Some(DefinedTableIndex::new(
|
||||
table.index() - self.imported_tables.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether the given table index is for an imported table.
|
||||
pub fn is_imported_table(&self, index: TableIndex) -> bool {
|
||||
index.index() < self.imported_tables.len()
|
||||
}
|
||||
|
||||
/// Convert a `DefinedMemoryIndex` into a `MemoryIndex`.
|
||||
pub fn memory_index(&self, defined_memory: DefinedMemoryIndex) -> MemoryIndex {
|
||||
MemoryIndex::new(self.imported_memories.len() + defined_memory.index())
|
||||
}
|
||||
|
||||
/// Convert a `MemoryIndex` into a `DefinedMemoryIndex`. Returns None if the
|
||||
/// index is an imported memory.
|
||||
pub fn defined_memory_index(&self, memory: MemoryIndex) -> Option<DefinedMemoryIndex> {
|
||||
if memory.index() < self.imported_memories.len() {
|
||||
None
|
||||
} else {
|
||||
Some(DefinedMemoryIndex::new(
|
||||
memory.index() - self.imported_memories.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether the given memory index is for an imported memory.
|
||||
pub fn is_imported_memory(&self, index: MemoryIndex) -> bool {
|
||||
index.index() < self.imported_memories.len()
|
||||
}
|
||||
|
||||
/// Convert a `DefinedGlobalIndex` into a `GlobalIndex`.
|
||||
pub fn global_index(&self, defined_global: DefinedGlobalIndex) -> GlobalIndex {
|
||||
GlobalIndex::new(self.imported_globals.len() + defined_global.index())
|
||||
}
|
||||
|
||||
/// Convert a `GlobalIndex` into a `DefinedGlobalIndex`. Returns None if the
|
||||
/// index is an imported global.
|
||||
pub fn defined_global_index(&self, global: GlobalIndex) -> Option<DefinedGlobalIndex> {
|
||||
if global.index() < self.imported_globals.len() {
|
||||
None
|
||||
} else {
|
||||
Some(DefinedGlobalIndex::new(
|
||||
global.index() - self.imported_globals.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether the given global index is for an imported global.
|
||||
pub fn is_imported_global(&self, index: GlobalIndex) -> bool {
|
||||
index.index() < self.imported_globals.len()
|
||||
}
|
||||
|
||||
/// Computes hash of the module for the purpose of caching.
|
||||
pub fn hash_for_cache<'data, H>(
|
||||
&self,
|
||||
function_body_inputs: &PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
state: &mut H,
|
||||
) where
|
||||
H: Hasher,
|
||||
{
|
||||
// There's no need to cache names (strings), start function
|
||||
// and data initializers (for both memory and tables)
|
||||
self.signatures.hash(state);
|
||||
self.functions.hash(state);
|
||||
self.table_plans.hash(state);
|
||||
self.memory_plans.hash(state);
|
||||
self.globals.hash(state);
|
||||
// IndexMap (self.export) iterates over values in order of item inserts
|
||||
// Let's actually sort the values.
|
||||
let mut exports = self.exports.values().collect::<Vec<_>>();
|
||||
exports.sort();
|
||||
for val in exports {
|
||||
val.hash(state);
|
||||
}
|
||||
function_body_inputs.hash(state);
|
||||
}
|
||||
}
|
||||
396
crates/environ/src/module_environ.rs
Normal file
396
crates/environ/src/module_environ.rs
Normal file
@@ -0,0 +1,396 @@
|
||||
use crate::func_environ::FuncEnvironment;
|
||||
use crate::module::{Export, MemoryPlan, Module, TableElements, TablePlan};
|
||||
use crate::tunables::Tunables;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::TryFrom;
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_codegen::ir::{AbiParam, ArgumentPurpose};
|
||||
use cranelift_codegen::isa::TargetFrontendConfig;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::{
|
||||
self, translate_module, DefinedFuncIndex, FuncIndex, Global, GlobalIndex, Memory, MemoryIndex,
|
||||
ModuleTranslationState, SignatureIndex, Table, TableIndex, WasmResult,
|
||||
};
|
||||
|
||||
/// Contains function data: byte code and its offset in the module.
|
||||
#[derive(Hash)]
|
||||
pub struct FunctionBodyData<'a> {
|
||||
/// Body byte code.
|
||||
pub data: &'a [u8],
|
||||
|
||||
/// Body offset in the module file.
|
||||
pub module_offset: usize,
|
||||
}
|
||||
|
||||
/// The result of translating via `ModuleEnvironment`. Function bodies are not
|
||||
/// yet translated, and data initializers have not yet been copied out of the
|
||||
/// original buffer.
|
||||
pub struct ModuleTranslation<'data> {
|
||||
/// Compilation setting flags.
|
||||
pub target_config: TargetFrontendConfig,
|
||||
|
||||
/// Module information.
|
||||
pub module: Module,
|
||||
|
||||
/// References to the function bodies.
|
||||
pub function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
|
||||
/// References to the data initializers.
|
||||
pub data_initializers: Vec<DataInitializer<'data>>,
|
||||
|
||||
/// Tunable parameters.
|
||||
pub tunables: Tunables,
|
||||
|
||||
/// The decoded Wasm types for the module.
|
||||
pub module_translation: Option<ModuleTranslationState>,
|
||||
}
|
||||
|
||||
impl<'data> ModuleTranslation<'data> {
|
||||
/// Return a new `FuncEnvironment` for translating a function.
|
||||
pub fn func_env(&self) -> FuncEnvironment<'_> {
|
||||
FuncEnvironment::new(self.target_config, &self.module)
|
||||
}
|
||||
}
|
||||
|
||||
/// Object containing the standalone environment information.
|
||||
pub struct ModuleEnvironment<'data> {
|
||||
/// The result to be filled in.
|
||||
result: ModuleTranslation<'data>,
|
||||
}
|
||||
|
||||
impl<'data> ModuleEnvironment<'data> {
|
||||
/// Allocates the enironment data structures.
|
||||
pub fn new(target_config: TargetFrontendConfig, tunables: Tunables) -> Self {
|
||||
Self {
|
||||
result: ModuleTranslation {
|
||||
target_config,
|
||||
module: Module::new(),
|
||||
function_body_inputs: PrimaryMap::new(),
|
||||
data_initializers: Vec::new(),
|
||||
tunables,
|
||||
module_translation: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn pointer_type(&self) -> ir::Type {
|
||||
self.result.target_config.pointer_type()
|
||||
}
|
||||
|
||||
/// Translate a wasm module using this environment. This consumes the
|
||||
/// `ModuleEnvironment` and produces a `ModuleTranslation`.
|
||||
pub fn translate(mut self, data: &'data [u8]) -> WasmResult<ModuleTranslation<'data>> {
|
||||
assert!(self.result.module_translation.is_none());
|
||||
let module_translation = translate_module(data, &mut self)?;
|
||||
self.result.module_translation = Some(module_translation);
|
||||
Ok(self.result)
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait is useful for `translate_module` because it tells how to translate
|
||||
/// enironment-dependent wasm instructions. These functions should not be called by the user.
|
||||
impl<'data> cranelift_wasm::ModuleEnvironment<'data> for ModuleEnvironment<'data> {
|
||||
fn target_config(&self) -> TargetFrontendConfig {
|
||||
self.result.target_config
|
||||
}
|
||||
|
||||
fn reserve_signatures(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.signatures
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_signature(&mut self, sig: ir::Signature) -> WasmResult<()> {
|
||||
let sig = translate_signature(sig, self.pointer_type());
|
||||
// TODO: Deduplicate signatures.
|
||||
self.result.module.signatures.push(sig);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_func_import(
|
||||
&mut self,
|
||||
sig_index: SignatureIndex,
|
||||
module: &str,
|
||||
field: &str,
|
||||
) -> WasmResult<()> {
|
||||
debug_assert_eq!(
|
||||
self.result.module.functions.len(),
|
||||
self.result.module.imported_funcs.len(),
|
||||
"Imported functions must be declared first"
|
||||
);
|
||||
self.result.module.functions.push(sig_index);
|
||||
|
||||
self.result
|
||||
.module
|
||||
.imported_funcs
|
||||
.push((String::from(module), String::from(field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_table_import(&mut self, table: Table, module: &str, field: &str) -> WasmResult<()> {
|
||||
debug_assert_eq!(
|
||||
self.result.module.table_plans.len(),
|
||||
self.result.module.imported_tables.len(),
|
||||
"Imported tables must be declared first"
|
||||
);
|
||||
let plan = TablePlan::for_table(table, &self.result.tunables);
|
||||
self.result.module.table_plans.push(plan);
|
||||
|
||||
self.result
|
||||
.module
|
||||
.imported_tables
|
||||
.push((String::from(module), String::from(field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_memory_import(
|
||||
&mut self,
|
||||
memory: Memory,
|
||||
module: &str,
|
||||
field: &str,
|
||||
) -> WasmResult<()> {
|
||||
debug_assert_eq!(
|
||||
self.result.module.memory_plans.len(),
|
||||
self.result.module.imported_memories.len(),
|
||||
"Imported memories must be declared first"
|
||||
);
|
||||
let plan = MemoryPlan::for_memory(memory, &self.result.tunables);
|
||||
self.result.module.memory_plans.push(plan);
|
||||
|
||||
self.result
|
||||
.module
|
||||
.imported_memories
|
||||
.push((String::from(module), String::from(field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_global_import(
|
||||
&mut self,
|
||||
global: Global,
|
||||
module: &str,
|
||||
field: &str,
|
||||
) -> WasmResult<()> {
|
||||
debug_assert_eq!(
|
||||
self.result.module.globals.len(),
|
||||
self.result.module.imported_globals.len(),
|
||||
"Imported globals must be declared first"
|
||||
);
|
||||
self.result.module.globals.push(global);
|
||||
|
||||
self.result
|
||||
.module
|
||||
.imported_globals
|
||||
.push((String::from(module), String::from(field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_imports(&mut self) -> WasmResult<()> {
|
||||
self.result.module.imported_funcs.shrink_to_fit();
|
||||
self.result.module.imported_tables.shrink_to_fit();
|
||||
self.result.module.imported_memories.shrink_to_fit();
|
||||
self.result.module.imported_globals.shrink_to_fit();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_func_types(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.functions
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
self.result
|
||||
.function_body_inputs
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_func_type(&mut self, sig_index: SignatureIndex) -> WasmResult<()> {
|
||||
self.result.module.functions.push(sig_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_tables(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.table_plans
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_table(&mut self, table: Table) -> WasmResult<()> {
|
||||
let plan = TablePlan::for_table(table, &self.result.tunables);
|
||||
self.result.module.table_plans.push(plan);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_memories(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.memory_plans
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_memory(&mut self, memory: Memory) -> WasmResult<()> {
|
||||
let plan = MemoryPlan::for_memory(memory, &self.result.tunables);
|
||||
self.result.module.memory_plans.push(plan);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_globals(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.globals
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_global(&mut self, global: Global) -> WasmResult<()> {
|
||||
self.result.module.globals.push(global);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_exports(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.exports
|
||||
.reserve(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_func_export(&mut self, func_index: FuncIndex, name: &str) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.exports
|
||||
.insert(String::from(name), Export::Function(func_index));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_table_export(&mut self, table_index: TableIndex, name: &str) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.exports
|
||||
.insert(String::from(name), Export::Table(table_index));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_memory_export(&mut self, memory_index: MemoryIndex, name: &str) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.exports
|
||||
.insert(String::from(name), Export::Memory(memory_index));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_global_export(&mut self, global_index: GlobalIndex, name: &str) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.exports
|
||||
.insert(String::from(name), Export::Global(global_index));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_start_func(&mut self, func_index: FuncIndex) -> WasmResult<()> {
|
||||
debug_assert!(self.result.module.start_func.is_none());
|
||||
self.result.module.start_func = Some(func_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_table_elements(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.table_elements
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_table_elements(
|
||||
&mut self,
|
||||
table_index: TableIndex,
|
||||
base: Option<GlobalIndex>,
|
||||
offset: usize,
|
||||
elements: Box<[FuncIndex]>,
|
||||
) -> WasmResult<()> {
|
||||
self.result.module.table_elements.push(TableElements {
|
||||
table_index,
|
||||
base,
|
||||
offset,
|
||||
elements,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn define_function_body(
|
||||
&mut self,
|
||||
_module_translation: &ModuleTranslationState,
|
||||
body_bytes: &'data [u8],
|
||||
body_offset: usize,
|
||||
) -> WasmResult<()> {
|
||||
self.result.function_body_inputs.push(FunctionBodyData {
|
||||
data: body_bytes,
|
||||
module_offset: body_offset,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_data_initializers(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.data_initializers
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_data_initialization(
|
||||
&mut self,
|
||||
memory_index: MemoryIndex,
|
||||
base: Option<GlobalIndex>,
|
||||
offset: usize,
|
||||
data: &'data [u8],
|
||||
) -> WasmResult<()> {
|
||||
self.result.data_initializers.push(DataInitializer {
|
||||
location: DataInitializerLocation {
|
||||
memory_index,
|
||||
base,
|
||||
offset,
|
||||
},
|
||||
data,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add environment-specific function parameters.
|
||||
pub fn translate_signature(mut sig: ir::Signature, pointer_type: ir::Type) -> ir::Signature {
|
||||
// Prepend the vmctx argument.
|
||||
sig.params.insert(
|
||||
0,
|
||||
AbiParam::special(pointer_type, ArgumentPurpose::VMContext),
|
||||
);
|
||||
sig
|
||||
}
|
||||
|
||||
/// A memory index and offset within that memory where a data initialization
|
||||
/// should is to be performed.
|
||||
#[derive(Clone)]
|
||||
pub struct DataInitializerLocation {
|
||||
/// The index of the memory to initialize.
|
||||
pub memory_index: MemoryIndex,
|
||||
|
||||
/// Optionally a globalvar base to initialize at.
|
||||
pub base: Option<GlobalIndex>,
|
||||
|
||||
/// A constant offset to initialize at.
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
/// A data initializer for linear memory.
|
||||
pub struct DataInitializer<'data> {
|
||||
/// The location where the initialization is to be performed.
|
||||
pub location: DataInitializerLocation,
|
||||
|
||||
/// The initialization data.
|
||||
pub data: &'data [u8],
|
||||
}
|
||||
44
crates/environ/src/tunables.rs
Normal file
44
crates/environ/src/tunables.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
/// Tunable parameters for WebAssembly compilation.
|
||||
#[derive(Clone)]
|
||||
pub struct Tunables {
|
||||
/// For static heaps, the size of the heap protected by bounds checking.
|
||||
pub static_memory_bound: u32,
|
||||
|
||||
/// The size of the offset guard for static heaps.
|
||||
pub static_memory_offset_guard_size: u64,
|
||||
|
||||
/// The size of the offset guard for dynamic heaps.
|
||||
pub dynamic_memory_offset_guard_size: u64,
|
||||
}
|
||||
|
||||
impl Default for Tunables {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
/// Size in wasm pages of the bound for static memories.
|
||||
static_memory_bound: 0x4000,
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
/// Size in wasm pages of the bound for static memories.
|
||||
///
|
||||
/// When we allocate 4 GiB of address space, we can avoid the
|
||||
/// need for explicit bounds checks.
|
||||
static_memory_bound: 0x1_0000,
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
/// Size in bytes of the offset guard for static memories.
|
||||
static_memory_offset_guard_size: 0x1_0000,
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
/// Size in bytes of the offset guard for static memories.
|
||||
///
|
||||
/// Allocating 2 GiB of address space lets us translate wasm
|
||||
/// offsets into x86 offsets as aggressively as we can.
|
||||
static_memory_offset_guard_size: 0x8000_0000,
|
||||
|
||||
/// Size in bytes of the offset guard for dynamic memories.
|
||||
///
|
||||
/// Allocate a small guard to optimize common cases but without
|
||||
/// wasting too much memor.
|
||||
dynamic_memory_offset_guard_size: 0x1_0000,
|
||||
}
|
||||
}
|
||||
}
|
||||
583
crates/environ/src/vmoffsets.rs
Normal file
583
crates/environ/src/vmoffsets.rs
Normal file
@@ -0,0 +1,583 @@
|
||||
//! Offsets and sizes of various structs in wasmtime-runtime's vmcontext
|
||||
//! module.
|
||||
|
||||
use crate::module::Module;
|
||||
use crate::BuiltinFunctionIndex;
|
||||
use core::convert::TryFrom;
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_wasm::{
|
||||
DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, FuncIndex, GlobalIndex, MemoryIndex,
|
||||
SignatureIndex, TableIndex,
|
||||
};
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
fn cast_to_u32(sz: usize) -> u32 {
|
||||
u32::try_from(sz).unwrap()
|
||||
}
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn cast_to_u32(sz: usize) -> u32 {
|
||||
match u32::try_from(sz) {
|
||||
Ok(x) => x,
|
||||
Err(_) => panic!("overflow in cast from usize to u32"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Align an offset used in this module to a specific byte-width by rounding up
|
||||
fn align(offset: u32, width: u32) -> u32 {
|
||||
(offset + (width - 1)) / width * width
|
||||
}
|
||||
|
||||
/// This class computes offsets to fields within `VMContext` and other
|
||||
/// related structs that JIT code accesses directly.
|
||||
pub struct VMOffsets {
|
||||
/// The size in bytes of a pointer on the target.
|
||||
pub pointer_size: u8,
|
||||
/// The number of signature declarations in the module.
|
||||
pub num_signature_ids: u32,
|
||||
/// The number of imported functions in the module.
|
||||
pub num_imported_functions: u32,
|
||||
/// The number of imported tables in the module.
|
||||
pub num_imported_tables: u32,
|
||||
/// The number of imported memories in the module.
|
||||
pub num_imported_memories: u32,
|
||||
/// The number of imported globals in the module.
|
||||
pub num_imported_globals: u32,
|
||||
/// The number of defined tables in the module.
|
||||
pub num_defined_tables: u32,
|
||||
/// The number of defined memories in the module.
|
||||
pub num_defined_memories: u32,
|
||||
/// The number of defined globals in the module.
|
||||
pub num_defined_globals: u32,
|
||||
}
|
||||
|
||||
impl VMOffsets {
|
||||
/// Return a new `VMOffsets` instance, for a given pointer size.
|
||||
pub fn new(pointer_size: u8, module: &Module) -> Self {
|
||||
Self {
|
||||
pointer_size,
|
||||
num_signature_ids: cast_to_u32(module.signatures.len()),
|
||||
num_imported_functions: cast_to_u32(module.imported_funcs.len()),
|
||||
num_imported_tables: cast_to_u32(module.imported_tables.len()),
|
||||
num_imported_memories: cast_to_u32(module.imported_memories.len()),
|
||||
num_imported_globals: cast_to_u32(module.imported_globals.len()),
|
||||
num_defined_tables: cast_to_u32(module.table_plans.len()),
|
||||
num_defined_memories: cast_to_u32(module.memory_plans.len()),
|
||||
num_defined_globals: cast_to_u32(module.globals.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMFunctionImport`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `body` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmfunction_import_body(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `vmctx` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmfunction_import_vmctx(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// Return the size of `VMFunctionImport`.
|
||||
pub fn size_of_vmfunction_import(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `*const VMFunctionBody`.
|
||||
impl VMOffsets {
|
||||
/// The size of the `current_elements` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn size_of_vmfunction_body_ptr(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMTableImport`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `from` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmtable_import_from(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `vmctx` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmtable_import_vmctx(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// Return the size of `VMTableImport`.
|
||||
pub fn size_of_vmtable_import(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMTableDefinition`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `base` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmtable_definition_base(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `current_elements` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmtable_definition_current_elements(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The size of the `current_elements` field.
|
||||
pub fn size_of_vmtable_definition_current_elements(&self) -> u8 {
|
||||
4
|
||||
}
|
||||
|
||||
/// Return the size of `VMTableDefinition`.
|
||||
pub fn size_of_vmtable_definition(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The type of the `current_elements` field.
|
||||
pub fn type_of_vmtable_definition_current_elements(&self) -> ir::Type {
|
||||
ir::Type::int(u16::from(self.size_of_vmtable_definition_current_elements()) * 8).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMMemoryImport`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `from` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmmemory_import_from(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `vmctx` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmmemory_import_vmctx(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// Return the size of `VMMemoryImport`.
|
||||
pub fn size_of_vmmemory_import(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMMemoryDefinition`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `base` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmmemory_definition_base(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `current_length` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmmemory_definition_current_length(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The size of the `current_length` field.
|
||||
pub fn size_of_vmmemory_definition_current_length(&self) -> u8 {
|
||||
4
|
||||
}
|
||||
|
||||
/// Return the size of `VMMemoryDefinition`.
|
||||
pub fn size_of_vmmemory_definition(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The type of the `current_length` field.
|
||||
pub fn type_of_vmmemory_definition_current_length(&self) -> ir::Type {
|
||||
ir::Type::int(u16::from(self.size_of_vmmemory_definition_current_length()) * 8).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMGlobalImport`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `from` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmglobal_import_from(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// Return the size of `VMGlobalImport`.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn size_of_vmglobal_import(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMGlobalDefinition`.
|
||||
impl VMOffsets {
|
||||
/// Return the size of `VMGlobalDefinition`; this is the size of the largest value type (i.e. a
|
||||
/// V128).
|
||||
pub fn size_of_vmglobal_definition(&self) -> u8 {
|
||||
16
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMSharedSignatureIndex`.
|
||||
impl VMOffsets {
|
||||
/// Return the size of `VMSharedSignatureIndex`.
|
||||
pub fn size_of_vmshared_signature_index(&self) -> u8 {
|
||||
4
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMCallerCheckedAnyfunc`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `func_ptr` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmcaller_checked_anyfunc_func_ptr(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `type_index` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmcaller_checked_anyfunc_type_index(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `vmctx` field.
|
||||
pub fn vmcaller_checked_anyfunc_vmctx(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
|
||||
/// Return the size of `VMCallerCheckedAnyfunc`.
|
||||
pub fn size_of_vmcaller_checked_anyfunc(&self) -> u8 {
|
||||
3 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMContext`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `signature_ids` array.
|
||||
pub fn vmctx_signature_ids_begin(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// The offset of the `tables` array.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmctx_imported_functions_begin(&self) -> u32 {
|
||||
self.vmctx_signature_ids_begin()
|
||||
.checked_add(
|
||||
self.num_signature_ids
|
||||
.checked_mul(u32::from(self.size_of_vmshared_signature_index()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `tables` array.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmctx_imported_tables_begin(&self) -> u32 {
|
||||
self.vmctx_imported_functions_begin()
|
||||
.checked_add(
|
||||
self.num_imported_functions
|
||||
.checked_mul(u32::from(self.size_of_vmfunction_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `memories` array.
|
||||
pub fn vmctx_imported_memories_begin(&self) -> u32 {
|
||||
self.vmctx_imported_tables_begin()
|
||||
.checked_add(
|
||||
self.num_imported_tables
|
||||
.checked_mul(u32::from(self.size_of_vmtable_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `globals` array.
|
||||
pub fn vmctx_imported_globals_begin(&self) -> u32 {
|
||||
self.vmctx_imported_memories_begin()
|
||||
.checked_add(
|
||||
self.num_imported_memories
|
||||
.checked_mul(u32::from(self.size_of_vmmemory_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `tables` array.
|
||||
pub fn vmctx_tables_begin(&self) -> u32 {
|
||||
self.vmctx_imported_globals_begin()
|
||||
.checked_add(
|
||||
self.num_imported_globals
|
||||
.checked_mul(u32::from(self.size_of_vmglobal_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `memories` array.
|
||||
pub fn vmctx_memories_begin(&self) -> u32 {
|
||||
self.vmctx_tables_begin()
|
||||
.checked_add(
|
||||
self.num_defined_tables
|
||||
.checked_mul(u32::from(self.size_of_vmtable_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `globals` array.
|
||||
pub fn vmctx_globals_begin(&self) -> u32 {
|
||||
let offset = self
|
||||
.vmctx_memories_begin()
|
||||
.checked_add(
|
||||
self.num_defined_memories
|
||||
.checked_mul(u32::from(self.size_of_vmmemory_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
align(offset, 16)
|
||||
}
|
||||
|
||||
/// The offset of the builtin functions array.
|
||||
pub fn vmctx_builtin_functions_begin(&self) -> u32 {
|
||||
self.vmctx_globals_begin()
|
||||
.checked_add(
|
||||
self.num_defined_globals
|
||||
.checked_mul(u32::from(self.size_of_vmglobal_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the size of the `VMContext` allocation.
|
||||
pub fn size_of_vmctx(&self) -> u32 {
|
||||
self.vmctx_builtin_functions_begin()
|
||||
.checked_add(
|
||||
BuiltinFunctionIndex::builtin_functions_total_number()
|
||||
.checked_mul(u32::from(self.pointer_size))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMSharedSignatureId` index `index`.
|
||||
pub fn vmctx_vmshared_signature_id(&self, index: SignatureIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_signature_ids);
|
||||
self.vmctx_signature_ids_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmshared_signature_index()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMFunctionImport` index `index`.
|
||||
pub fn vmctx_vmfunction_import(&self, index: FuncIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_imported_functions);
|
||||
self.vmctx_imported_functions_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmfunction_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMTableImport` index `index`.
|
||||
pub fn vmctx_vmtable_import(&self, index: TableIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_imported_tables);
|
||||
self.vmctx_imported_tables_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmtable_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMMemoryImport` index `index`.
|
||||
pub fn vmctx_vmmemory_import(&self, index: MemoryIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_imported_memories);
|
||||
self.vmctx_imported_memories_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmmemory_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMGlobalImport` index `index`.
|
||||
pub fn vmctx_vmglobal_import(&self, index: GlobalIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_imported_globals);
|
||||
self.vmctx_imported_globals_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmglobal_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMTableDefinition` index `index`.
|
||||
pub fn vmctx_vmtable_definition(&self, index: DefinedTableIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_defined_tables);
|
||||
self.vmctx_tables_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmtable_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMMemoryDefinition` index `index`.
|
||||
pub fn vmctx_vmmemory_definition(&self, index: DefinedMemoryIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_defined_memories);
|
||||
self.vmctx_memories_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmmemory_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `VMGlobalDefinition` index `index`.
|
||||
pub fn vmctx_vmglobal_definition(&self, index: DefinedGlobalIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_defined_globals);
|
||||
self.vmctx_globals_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmglobal_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `body` field in `*const VMFunctionBody` index `index`.
|
||||
pub fn vmctx_vmfunction_import_body(&self, index: FuncIndex) -> u32 {
|
||||
self.vmctx_vmfunction_import(index)
|
||||
.checked_add(u32::from(self.vmfunction_import_body()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `vmctx` field in `*const VMFunctionBody` index `index`.
|
||||
pub fn vmctx_vmfunction_import_vmctx(&self, index: FuncIndex) -> u32 {
|
||||
self.vmctx_vmfunction_import(index)
|
||||
.checked_add(u32::from(self.vmfunction_import_vmctx()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `from` field in `VMTableImport` index `index`.
|
||||
pub fn vmctx_vmtable_import_from(&self, index: TableIndex) -> u32 {
|
||||
self.vmctx_vmtable_import(index)
|
||||
.checked_add(u32::from(self.vmtable_import_from()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `base` field in `VMTableDefinition` index `index`.
|
||||
pub fn vmctx_vmtable_definition_base(&self, index: DefinedTableIndex) -> u32 {
|
||||
self.vmctx_vmtable_definition(index)
|
||||
.checked_add(u32::from(self.vmtable_definition_base()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `current_elements` field in `VMTableDefinition` index `index`.
|
||||
pub fn vmctx_vmtable_definition_current_elements(&self, index: DefinedTableIndex) -> u32 {
|
||||
self.vmctx_vmtable_definition(index)
|
||||
.checked_add(u32::from(self.vmtable_definition_current_elements()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `from` field in `VMMemoryImport` index `index`.
|
||||
pub fn vmctx_vmmemory_import_from(&self, index: MemoryIndex) -> u32 {
|
||||
self.vmctx_vmmemory_import(index)
|
||||
.checked_add(u32::from(self.vmmemory_import_from()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `vmctx` field in `VMMemoryImport` index `index`.
|
||||
pub fn vmctx_vmmemory_import_vmctx(&self, index: MemoryIndex) -> u32 {
|
||||
self.vmctx_vmmemory_import(index)
|
||||
.checked_add(u32::from(self.vmmemory_import_vmctx()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `base` field in `VMMemoryDefinition` index `index`.
|
||||
pub fn vmctx_vmmemory_definition_base(&self, index: DefinedMemoryIndex) -> u32 {
|
||||
self.vmctx_vmmemory_definition(index)
|
||||
.checked_add(u32::from(self.vmmemory_definition_base()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `current_length` field in `VMMemoryDefinition` index `index`.
|
||||
pub fn vmctx_vmmemory_definition_current_length(&self, index: DefinedMemoryIndex) -> u32 {
|
||||
self.vmctx_vmmemory_definition(index)
|
||||
.checked_add(u32::from(self.vmmemory_definition_current_length()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `from` field in `VMGlobalImport` index `index`.
|
||||
pub fn vmctx_vmglobal_import_from(&self, index: GlobalIndex) -> u32 {
|
||||
self.vmctx_vmglobal_import(index)
|
||||
.checked_add(u32::from(self.vmglobal_import_from()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to builtin function in `VMBuiltinFunctionsArray` index `index`.
|
||||
pub fn vmctx_builtin_function(&self, index: BuiltinFunctionIndex) -> u32 {
|
||||
self.vmctx_builtin_functions_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.index()
|
||||
.checked_mul(u32::from(self.pointer_size))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Target specific type for shared signature index.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TargetSharedSignatureIndex(u32);
|
||||
|
||||
impl TargetSharedSignatureIndex {
|
||||
/// Constructs `TargetSharedSignatureIndex`.
|
||||
pub fn new(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Returns index value.
|
||||
pub fn index(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::vmoffsets::align;
|
||||
|
||||
#[test]
|
||||
fn alignment() {
|
||||
fn is_aligned(x: u32) -> bool {
|
||||
x % 16 == 0
|
||||
}
|
||||
assert!(is_aligned(align(0, 16)));
|
||||
assert!(is_aligned(align(32, 16)));
|
||||
assert!(is_aligned(align(33, 16)));
|
||||
assert!(is_aligned(align(31, 16)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user