diff --git a/crates/bench-api/src/lib.rs b/crates/bench-api/src/lib.rs index ff6b88be310a..4c0f5de7aed6 100644 --- a/crates/bench-api/src/lib.rs +++ b/crates/bench-api/src/lib.rs @@ -436,7 +436,7 @@ impl BenchState { ) -> Result { let mut config = options.config(None)?; // NB: always disable the compilation cache. - config.disable_cache(); + config.cache(None); let engine = Engine::new(&config)?; let mut linker = Linker::::new(&engine); diff --git a/crates/c-api/src/config.rs b/crates/c-api/src/config.rs index 53baad88152c..2ce7cba95928 100644 --- a/crates/c-api/src/config.rs +++ b/crates/c-api/src/config.rs @@ -211,12 +211,18 @@ pub unsafe extern "C" fn wasmtime_config_cache_config_load( c: &mut wasm_config_t, filename: *const c_char, ) -> Option> { + use std::path::Path; + + use wasmtime::Cache; + handle_result( if filename.is_null() { - c.config.cache_config_load_default() + Cache::from_file(None).map(|cache| c.config.cache(Some(cache))) } else { match CStr::from_ptr(filename).to_str() { - Ok(s) => c.config.cache_config_load(s), + Ok(s) => { + Cache::from_file(Some(&Path::new(s))).map(|cache| c.config.cache(Some(cache))) + } Err(e) => Err(e.into()), } }, diff --git a/crates/cache/src/config.rs b/crates/cache/src/config.rs index 62fc6e936f8e..278ca3e7ad0a 100644 --- a/crates/cache/src/config.rs +++ b/crates/cache/src/config.rs @@ -1,6 +1,5 @@ //! Module for configuring the cache system. -use super::Worker; use anyhow::{anyhow, bail, Context, Result}; use directories_next::ProjectDirs; use log::{trace, warn}; @@ -11,8 +10,6 @@ use serde::{ use std::fmt::Debug; use std::fs; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; -use std::sync::Arc; use std::time::Duration; // wrapped, so we have named section in config, @@ -27,77 +24,93 @@ struct Config { #[derive(serde_derive::Deserialize, Debug, Clone)] #[serde(deny_unknown_fields)] pub struct CacheConfig { - enabled: bool, directory: Option, #[serde( - default, + default = "default_worker_event_queue_size", rename = "worker-event-queue-size", deserialize_with = "deserialize_si_prefix" )] - worker_event_queue_size: Option, - #[serde(rename = "baseline-compression-level")] - baseline_compression_level: Option, - #[serde(rename = "optimized-compression-level")] - optimized_compression_level: Option, + worker_event_queue_size: u64, #[serde( - default, + default = "default_baseline_compression_level", + rename = "baseline-compression-level" + )] + baseline_compression_level: i32, + #[serde( + default = "default_optimized_compression_level", + rename = "optimized-compression-level" + )] + optimized_compression_level: i32, + #[serde( + default = "default_optimized_compression_usage_counter_threshold", rename = "optimized-compression-usage-counter-threshold", deserialize_with = "deserialize_si_prefix" )] - optimized_compression_usage_counter_threshold: Option, + optimized_compression_usage_counter_threshold: u64, #[serde( - default, + default = "default_cleanup_interval", rename = "cleanup-interval", deserialize_with = "deserialize_duration" )] - cleanup_interval: Option, + cleanup_interval: Duration, #[serde( - default, + default = "default_optimizing_compression_task_timeout", rename = "optimizing-compression-task-timeout", deserialize_with = "deserialize_duration" )] - optimizing_compression_task_timeout: Option, + optimizing_compression_task_timeout: Duration, #[serde( - default, + default = "default_allowed_clock_drift_for_files_from_future", rename = "allowed-clock-drift-for-files-from-future", deserialize_with = "deserialize_duration" )] - allowed_clock_drift_for_files_from_future: Option, + allowed_clock_drift_for_files_from_future: Duration, #[serde( - default, + default = "default_file_count_soft_limit", rename = "file-count-soft-limit", deserialize_with = "deserialize_si_prefix" )] - file_count_soft_limit: Option, + file_count_soft_limit: u64, #[serde( - default, + default = "default_files_total_size_soft_limit", rename = "files-total-size-soft-limit", deserialize_with = "deserialize_disk_space" )] - files_total_size_soft_limit: Option, + files_total_size_soft_limit: u64, #[serde( - default, + default = "default_file_count_limit_percent_if_deleting", rename = "file-count-limit-percent-if-deleting", deserialize_with = "deserialize_percent" )] - file_count_limit_percent_if_deleting: Option, + file_count_limit_percent_if_deleting: u8, #[serde( - default, + default = "default_files_total_size_limit_percent_if_deleting", rename = "files-total-size-limit-percent-if-deleting", deserialize_with = "deserialize_percent" )] - files_total_size_limit_percent_if_deleting: Option, - - #[serde(skip)] - worker: Option, - #[serde(skip)] - state: Arc, + files_total_size_limit_percent_if_deleting: u8, } -#[derive(Default, Debug)] -struct CacheState { - hits: AtomicUsize, - misses: AtomicUsize, +impl Default for CacheConfig { + fn default() -> Self { + Self { + directory: None, + worker_event_queue_size: default_worker_event_queue_size(), + baseline_compression_level: default_baseline_compression_level(), + optimized_compression_level: default_optimized_compression_level(), + optimized_compression_usage_counter_threshold: + default_optimized_compression_usage_counter_threshold(), + cleanup_interval: default_cleanup_interval(), + optimizing_compression_task_timeout: default_optimizing_compression_task_timeout(), + allowed_clock_drift_for_files_from_future: + default_allowed_clock_drift_for_files_from_future(), + file_count_soft_limit: default_file_count_soft_limit(), + files_total_size_soft_limit: default_files_total_size_soft_limit(), + file_count_limit_percent_if_deleting: default_file_count_limit_percent_if_deleting(), + files_total_size_limit_percent_if_deleting: + default_files_total_size_limit_percent_if_deleting(), + } + } } /// Creates a new configuration file at specified path, or default path if None is passed. @@ -134,7 +147,6 @@ pub fn create_new_config + Debug>(config_file: Option

) -> Resu # https://bytecodealliance.github.io/wasmtime/cli-cache.html [cache] -enabled = true "; fs::write(&config_file, content).with_context(|| { @@ -156,34 +168,57 @@ const ZSTD_COMPRESSION_LEVELS: std::ops::RangeInclusive = 0..=21; // At the moment of writing, the modules couldn't depend on another, // so we have at most one module per wasmtime instance // if changed, update cli-cache.md -const DEFAULT_WORKER_EVENT_QUEUE_SIZE: u64 = 0x10; -const WORKER_EVENT_QUEUE_SIZE_WARNING_THRESHOLD: u64 = 3; +const fn default_worker_event_queue_size() -> u64 { + 0x10 +} +const fn worker_event_queue_size_warning_threshold() -> u64 { + 3 +} // should be quick and provide good enough compression // if changed, update cli-cache.md -const DEFAULT_BASELINE_COMPRESSION_LEVEL: i32 = zstd::DEFAULT_COMPRESSION_LEVEL; +const fn default_baseline_compression_level() -> i32 { + zstd::DEFAULT_COMPRESSION_LEVEL +} // should provide significantly better compression than baseline // if changed, update cli-cache.md -const DEFAULT_OPTIMIZED_COMPRESSION_LEVEL: i32 = 20; +const fn default_optimized_compression_level() -> i32 { + 20 +} // shouldn't be to low to avoid recompressing too many files // if changed, update cli-cache.md -const DEFAULT_OPTIMIZED_COMPRESSION_USAGE_COUNTER_THRESHOLD: u64 = 0x100; +const fn default_optimized_compression_usage_counter_threshold() -> u64 { + 0x100 +} // if changed, update cli-cache.md -const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60 * 60); +const fn default_cleanup_interval() -> Duration { + Duration::from_secs(60 * 60) +} // if changed, update cli-cache.md -const DEFAULT_OPTIMIZING_COMPRESSION_TASK_TIMEOUT: Duration = Duration::from_secs(30 * 60); +const fn 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 cli-cache.md -const DEFAULT_ALLOWED_CLOCK_DRIFT_FOR_FILES_FROM_FUTURE: Duration = - Duration::from_secs(60 * 60 * 24); +const fn default_allowed_clock_drift_for_files_from_future() -> Duration { + Duration::from_secs(60 * 60 * 24) +} // if changed, update cli-cache.md -const DEFAULT_FILE_COUNT_SOFT_LIMIT: u64 = 0x10_000; +const fn default_file_count_soft_limit() -> u64 { + 0x10_000 +} // if changed, update cli-cache.md -const DEFAULT_FILES_TOTAL_SIZE_SOFT_LIMIT: u64 = 1024 * 1024 * 512; +const fn default_files_total_size_soft_limit() -> u64 { + 1024 * 1024 * 512 +} // if changed, update cli-cache.md -const DEFAULT_FILE_COUNT_LIMIT_PERCENT_IF_DELETING: u8 = 70; +const fn default_file_count_limit_percent_if_deleting() -> u8 { + 70 +} // if changed, update cli-cache.md -const DEFAULT_FILES_TOTAL_SIZE_LIMIT_PERCENT_IF_DELETING: u8 = 70; +const fn default_files_total_size_limit_percent_if_deleting() -> u8 { + 70 +} fn project_dirs() -> Option { ProjectDirs::from("", "BytecodeAlliance", "wasmtime") @@ -204,11 +239,7 @@ macro_rules! generate_deserializer { where D: Deserializer<'de>, { - let text = Option::::deserialize(deserializer)?; - let text = match text { - None => return Ok(None), - Some(text) => text, - }; + let text = String::deserialize(deserializer)?; 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)); @@ -217,7 +248,7 @@ macro_rules! generate_deserializer { let $unitname = unit.trim(); $body })(); - if deserialized.is_some() { + if let Some(deserialized) = deserialized { Ok(deserialized) } else { Err(de::Error::custom( @@ -228,7 +259,7 @@ macro_rules! generate_deserializer { }; } -generate_deserializer!(deserialize_duration(num: u64, unit: &str) -> Option { +generate_deserializer!(deserialize_duration(num: u64, unit: &str) -> Duration { match unit { "s" => Some(Duration::from_secs(num)), "m" => Some(Duration::from_secs(num * 60)), @@ -238,7 +269,7 @@ generate_deserializer!(deserialize_duration(num: u64, unit: &str) -> Option Option { +generate_deserializer!(deserialize_si_prefix(num: u64, unit: &str) -> u64 { match unit { "" => Some(num), "K" => num.checked_mul(1_000), @@ -250,7 +281,7 @@ generate_deserializer!(deserialize_si_prefix(num: u64, unit: &str) -> Option Option { +generate_deserializer!(deserialize_disk_space(num: u64, unit: &str) -> u64 { match unit { "" => Some(num), "K" => num.checked_mul(1_000), @@ -267,7 +298,7 @@ generate_deserializer!(deserialize_disk_space(num: u64, unit: &str) -> Option Option { +generate_deserializer!(deserialize_percent(num: u8, unit: &str) -> u8 { match unit { "%" => Some(num), _ => None, @@ -283,12 +314,65 @@ macro_rules! generate_setting_getter { /// /// Panics if the cache is disabled. pub fn $setting(&self) -> $setting_type { - self.$setting.expect(CACHE_IMPROPER_CONFIG_ERROR_MSG) + self.$setting } }; } impl CacheConfig { + /// Creates a new set of configuration which represents a disabled cache + pub fn new() -> Self { + Self::default() + } + + /// Loads cache configuration specified at `path`. + /// + /// This method will read the file specified by `path` on the filesystem and + /// attempt to load cache configuration from it. This method can also fail + /// due to I/O errors, misconfiguration, syntax errors, etc. For expected + /// syntax in the configuration file see the [documentation online][docs]. + /// + /// Passing in `None` loads cache configuration from the system default path. + /// This is located, for example, on Unix at `$HOME/.config/wasmtime/config.toml` + /// and is typically created with the `wasmtime config new` command. + /// + /// # Errors + /// + /// This method can fail due to any error that happens when loading the file + /// pointed to by `path` and attempting to load the cache configuration. + /// + /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html + pub fn from_file(config_file: Option<&Path>) -> Result { + let mut config = Self::load_and_parse_file(config_file)?; + config.validate()?; + Ok(config) + } + + fn load_and_parse_file(config_file: Option<&Path>) -> Result { + // get config file path + let (config_file, user_custom_file) = match config_file { + Some(path) => (path.to_path_buf(), true), + None => (default_config_path()?, false), + }; + + // read config, or use default one + let entity_exists = config_file.exists(); + match (entity_exists, user_custom_file) { + (false, false) => Ok(Self::new()), + _ => { + let contents = fs::read_to_string(&config_file).context(format!( + "failed to read config file: {}", + config_file.display() + ))?; + let config = toml::from_str::(&contents).context(format!( + "failed to parse config file: {}", + config_file.display() + ))?; + Ok(config.cache) + } + } + } + generate_setting_getter!(worker_event_queue_size: u64); generate_setting_getter!(baseline_compression_level: i32); generate_setting_getter!(optimized_compression_level: i32); @@ -301,11 +385,6 @@ impl CacheConfig { 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. @@ -315,109 +394,133 @@ impl CacheConfig { .expect(CACHE_IMPROPER_CONFIG_ERROR_MSG) } - /// Creates a new set of configuration which represents a disabled cache - pub fn new_cache_disabled() -> Self { - Self { - 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, - worker: None, - state: Arc::new(CacheState::default()), - } + /// Specify where the cache directory is. Must be an absolute path. + pub fn with_directory(&mut self, directory: impl Into) -> &mut Self { + self.directory = Some(directory.into()); + self } - fn new_cache_enabled_template() -> Self { - let mut conf = Self::new_cache_disabled(); - conf.enabled = true; - conf + /// Size of cache worker event queue. If the queue is full, incoming cache usage events will be + /// dropped. + pub fn with_worker_event_queue_size(&mut self, size: u64) -> &mut Self { + self.worker_event_queue_size = size; + self } - /// Parses cache configuration from the file specified - pub fn from_file(config_file: Option<&Path>) -> Result { - let mut config = Self::load_and_parse_file(config_file)?; + /// Compression level used when a new cache file is being written by the cache system. Wasmtime + /// uses zstd compression. + pub fn with_baseline_compression_level(&mut self, level: i32) -> &mut Self { + self.baseline_compression_level = level; + self + } - // 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.spawn_worker(); + /// Compression level used when the cache worker decides to recompress a cache file. Wasmtime + /// uses zstd compression. + pub fn with_optimized_compression_level(&mut self, level: i32) -> &mut Self { + self.optimized_compression_level = level; + self + } - Ok(config) + /// One of the conditions for the cache worker to recompress a cache file is to have usage + /// count of the file exceeding this threshold. + pub fn with_optimized_compression_usage_counter_threshold( + &mut self, + threshold: u64, + ) -> &mut Self { + self.optimized_compression_usage_counter_threshold = threshold; + self } - fn spawn_worker(&mut self) { - if self.enabled { - self.worker = Some(Worker::start_new(self)); - } + /// When the cache worker is notified about a cache file being updated by the cache system and + /// this interval has already passed since last cleaning up, the worker will attempt a new + /// cleanup. + pub fn with_cleanup_interval(&mut self, interval: Duration) -> &mut Self { + self.cleanup_interval = interval; + self } - pub(super) fn worker(&self) -> &Worker { - assert!(self.enabled); - self.worker.as_ref().unwrap() + /// When the cache worker decides to recompress a cache file, it makes sure that no other + /// worker has started the task for this file within the last + /// optimizing-compression-task-timeout interval. If some worker has started working on it, + /// other workers are skipping this task. + pub fn with_optimizing_compression_task_timeout(&mut self, timeout: Duration) -> &mut Self { + self.optimizing_compression_task_timeout = timeout; + self } - /// Returns the number of cache hits seen so far - pub fn cache_hits(&self) -> usize { - self.state.hits.load(SeqCst) + /// ### Locks + /// + /// When the cache worker attempts acquiring a lock for some task, it checks if some other + /// worker has already acquired such a lock. To be fault tolerant and eventually execute every + /// task, the locks expire after some interval. However, because of clock drifts and different + /// timezones, it would happen that some lock was created in the future. This setting defines a + /// tolerance limit for these locks. If the time has been changed in the system (i.e. two years + /// backwards), the cache system should still work properly. Thus, these locks will be treated + /// as expired (assuming the tolerance is not too big). + /// + /// ### Cache files + /// + /// Similarly to the locks, the cache files or their metadata might have modification time in + /// distant future. The cache system tries to keep these files as long as possible. If the + /// limits are not reached, the cache files will not be deleted. Otherwise, they will be + /// treated as the oldest files, so they might survive. If the user actually uses the cache + /// file, the modification time will be updated. + pub fn with_allowed_clock_drift_for_files_from_future(&mut self, drift: Duration) -> &mut Self { + self.allowed_clock_drift_for_files_from_future = drift; + self } - /// Returns the number of cache misses seen so far - pub fn cache_misses(&self) -> usize { - self.state.misses.load(SeqCst) + /// Soft limit for the file count in the cache directory. + /// + /// This doesn't include files with metadata. To learn more, please refer to the cache system + /// section. + pub fn with_file_count_soft_limit(&mut self, limit: u64) -> &mut Self { + self.file_count_soft_limit = limit; + self } - pub(crate) fn on_cache_get_async(&self, path: impl AsRef) { - self.state.hits.fetch_add(1, SeqCst); - self.worker().on_cache_get_async(path) + /// Soft limit for the total size* of files in the cache directory. + /// + /// This doesn't include files with metadata. To learn more, please refer to the cache system + /// section. + /// + /// *this is the file size, not the space physically occupied on the disk. + pub fn with_files_total_size_soft_limit(&mut self, limit: u64) -> &mut Self { + self.files_total_size_soft_limit = limit; + self } - pub(crate) fn on_cache_update_async(&self, path: impl AsRef) { - self.state.misses.fetch_add(1, SeqCst); - self.worker().on_cache_update_async(path) + /// If file-count-soft-limit is exceeded and the cache worker performs the cleanup task, then + /// the worker will delete some cache files, so after the task, the file count should not + /// exceed file-count-soft-limit * file-count-limit-percent-if-deleting. + /// + /// This doesn't include files with metadata. To learn more, please refer to the cache system + /// section. + pub fn with_file_count_limit_percent_if_deleting(&mut self, percent: u8) -> &mut Self { + self.file_count_limit_percent_if_deleting = percent; + self } - fn load_and_parse_file(config_file: Option<&Path>) -> Result { - // get config file path - let (config_file, user_custom_file) = match config_file { - Some(path) => (path.to_path_buf(), true), - None => (default_config_path()?, false), - }; + /// If files-total-size-soft-limit is exceeded and cache worker performs the cleanup task, then + /// the worker will delete some cache files, so after the task, the files total size should not + /// exceed files-total-size-soft-limit * files-total-size-limit-percent-if-deleting. + /// + /// This doesn't include files with metadata. To learn more, please refer to the cache system + /// section. + pub fn with_files_total_size_limit_percent_if_deleting(&mut self, percent: u8) -> &mut Self { + self.files_total_size_limit_percent_if_deleting = percent; + self + } - // 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()), - _ => { - let contents = fs::read_to_string(&config_file).context(format!( - "failed to read config file: {}", - config_file.display() - ))?; - let config = toml::from_str::(&contents).context(format!( - "failed to parse config file: {}", - config_file.display() - ))?; - Ok(config.cache) - } - } + /// validate values and fill in defaults + pub(crate) fn validate(&mut self) -> Result<()> { + self.validate_directory_or_default()?; + self.validate_worker_event_queue_size(); + self.validate_baseline_compression_level()?; + self.validate_optimized_compression_level()?; + self.validate_file_count_limit_percent_if_deleting()?; + self.validate_files_total_size_limit_percent_if_deleting()?; + Ok(()) } fn validate_directory_or_default(&mut self) -> Result<()> { @@ -455,25 +558,17 @@ impl CacheConfig { Ok(()) } - 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_THRESHOLD { + fn validate_worker_event_queue_size(&self) { + if self.worker_event_queue_size < worker_event_queue_size_warning_threshold() { warn!("Detected small worker event queue size. Some messages might be lost."); } } - fn validate_baseline_compression_level_or_default(&mut self) -> Result<()> { - 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()) { + fn validate_baseline_compression_level(&self) -> Result<()> { + if !ZSTD_COMPRESSION_LEVELS.contains(&self.baseline_compression_level) { bail!( "Invalid baseline compression level: {} not in {:#?}", - self.baseline_compression_level.unwrap(), + self.baseline_compression_level, ZSTD_COMPRESSION_LEVELS ); } @@ -481,98 +576,40 @@ impl CacheConfig { } // assumption: baseline compression level has been verified - fn validate_optimized_compression_level_or_default(&mut self) -> Result<()> { - 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) { + fn validate_optimized_compression_level(&self) -> Result<()> { + if !ZSTD_COMPRESSION_LEVELS.contains(&self.optimized_compression_level) { bail!( "Invalid optimized compression level: {} not in {:#?}", - opt_lvl, + self.optimized_compression_level, ZSTD_COMPRESSION_LEVELS ); } - if opt_lvl < base_lvl { + if self.optimized_compression_level < self.baseline_compression_level { bail!( "Invalid optimized compression level is lower than baseline: {} < {}", - opt_lvl, - base_lvl + self.optimized_compression_level, + self.baseline_compression_level ); } Ok(()) } - 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) -> Result<()> { - 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 { + fn validate_file_count_limit_percent_if_deleting(&self) -> Result<()> { + if self.file_count_limit_percent_if_deleting > 100 { bail!( "Invalid files count limit percent if deleting: {} not in range 0-100%", - percent + self.file_count_limit_percent_if_deleting ); } Ok(()) } - fn validate_files_total_size_limit_percent_if_deleting_or_default(&mut self) -> Result<()> { - 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 { + fn validate_files_total_size_limit_percent_if_deleting(&self) -> Result<()> { + if self.files_total_size_limit_percent_if_deleting > 100 { bail!( "Invalid files total size limit percent if deleting: {} not in range 0-100%", - percent + self.files_total_size_limit_percent_if_deleting ); } Ok(()) diff --git a/crates/cache/src/config/tests.rs b/crates/cache/src/config/tests.rs index 9eae5e8657c2..45e50edbe421 100644 --- a/crates/cache/src/config/tests.rs +++ b/crates/cache/src/config/tests.rs @@ -34,18 +34,6 @@ macro_rules! bad_config { }}; } -// 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 = false\n"; - fs::write(&config_path, config_content).expect("Failed to write test config file"); - let conf = CacheConfig::from_file(Some(&config_path)).unwrap(); - assert!(!conf.enabled()); -} - #[test] fn test_unrecognized_settings() { let (_td, cd, cp) = test_prolog(); @@ -53,7 +41,6 @@ fn test_unrecognized_settings() { cp, "unrecognized-setting = 42\n\ [cache]\n\ - enabled = true\n\ directory = '{cache_dir}'", cd ); @@ -61,7 +48,6 @@ fn test_unrecognized_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ unrecognized-setting = 42", cd @@ -74,7 +60,6 @@ fn test_all_settings() { 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\ @@ -95,7 +80,6 @@ fn test_all_settings() { 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\ @@ -113,7 +97,6 @@ fn test_all_settings() { check_conf(&conf, &cd); fn check_conf(conf: &CacheConfig, cd: &PathBuf) { - assert!(conf.enabled()); assert_eq!( conf.directory(), &fs::canonicalize(cd).expect("canonicalize failed") @@ -144,20 +127,17 @@ fn test_compression_level_settings() { 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_eq!(conf.baseline_compression_level(), 1); assert_eq!(conf.optimized_compression_level(), 21); bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ baseline-compression-level = -1\n\ optimized-compression-level = 21", @@ -167,7 +147,6 @@ fn test_compression_level_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ baseline-compression-level = 15\n\ optimized-compression-level = 10", @@ -181,14 +160,12 @@ fn test_si_prefix_settings() { 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_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); @@ -196,14 +173,12 @@ fn test_si_prefix_settings() { let conf = load_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ worker-event-queue-size = '2K'\n\ optimized-compression-usage-counter-threshold = '4444T'\n\ file-count-soft-limit = '1P'", cd ); - assert!(conf.enabled()); assert_eq!(conf.worker_event_queue_size(), 2_000); assert_eq!( conf.optimized_compression_usage_counter_threshold(), @@ -215,7 +190,6 @@ fn test_si_prefix_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ worker-event-queue-size = '2g'", cd @@ -224,7 +198,6 @@ fn test_si_prefix_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ file-count-soft-limit = 1", cd @@ -233,7 +206,6 @@ fn test_si_prefix_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ file-count-soft-limit = '-31337'", cd @@ -242,7 +214,6 @@ fn test_si_prefix_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ file-count-soft-limit = '3.14M'", cd @@ -255,74 +226,61 @@ fn test_disk_space_settings() { 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_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_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_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_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_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_eq!(conf.files_total_size_soft_limit(), 7_000_000); // different errors bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ files-total-size-soft-limit = '7 mi'", cd @@ -331,7 +289,6 @@ fn test_disk_space_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ files-total-size-soft-limit = 1", cd @@ -340,7 +297,6 @@ fn test_disk_space_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ files-total-size-soft-limit = '-31337'", cd @@ -349,7 +305,6 @@ fn test_disk_space_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ files-total-size-soft-limit = '3.14Ki'", cd @@ -362,14 +317,12 @@ fn test_duration_settings() { 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_eq!(conf.cleanup_interval(), Duration::from_secs(100)); assert_eq!( conf.optimizing_compression_task_timeout(), @@ -383,13 +336,11 @@ fn test_duration_settings() { 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_eq!( conf.cleanup_interval(), Duration::from_secs(2 * 24 * 60 * 60) @@ -403,7 +354,6 @@ fn test_duration_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ optimizing-compression-task-timeout = '333'", cd @@ -412,7 +362,6 @@ fn test_duration_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ optimizing-compression-task-timeout = 333", cd @@ -421,7 +370,6 @@ fn test_duration_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ optimizing-compression-task-timeout = '10 M'", cd @@ -430,7 +378,6 @@ fn test_duration_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ optimizing-compression-task-timeout = '10 min'", cd @@ -439,7 +386,6 @@ fn test_duration_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ optimizing-compression-task-timeout = '-10s'", cd @@ -448,7 +394,6 @@ fn test_duration_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ optimizing-compression-task-timeout = '1.5m'", cd @@ -461,13 +406,11 @@ fn test_percent_settings() { 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_eq!(conf.file_count_limit_percent_if_deleting(), 62); assert_eq!(conf.files_total_size_limit_percent_if_deleting(), 23); @@ -475,7 +418,6 @@ fn test_percent_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ files-total-size-limit-percent-if-deleting = '23'", cd @@ -484,7 +426,6 @@ fn test_percent_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ files-total-size-limit-percent-if-deleting = '22.5%'", cd @@ -493,7 +434,6 @@ fn test_percent_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ files-total-size-limit-percent-if-deleting = '0.5'", cd @@ -502,7 +442,6 @@ fn test_percent_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ files-total-size-limit-percent-if-deleting = '-1%'", cd @@ -511,9 +450,110 @@ fn test_percent_settings() { bad_config!( cp, "[cache]\n\ - enabled = true\n\ directory = '{cache_dir}'\n\ files-total-size-limit-percent-if-deleting = '101%'", cd ); } + +/// Default builder produces a disabled cache configuration with the same defaults. +#[test] +fn test_builder_default() { + let (_td, _cd, cp) = test_prolog(); + let config_content = "[cache]\n"; + fs::write(&cp, config_content).expect("Failed to write test config file"); + let expected_config = CacheConfig::from_file(Some(&cp)).unwrap(); + + let mut config = CacheConfig::new(); + config + .validate() + .expect("Failed to validate default config"); + + assert_eq!(config.directory, expected_config.directory); + assert_eq!( + config.worker_event_queue_size, + expected_config.worker_event_queue_size + ); + assert_eq!( + config.baseline_compression_level, + expected_config.baseline_compression_level + ); + assert_eq!( + config.optimized_compression_level, + expected_config.optimized_compression_level + ); + assert_eq!( + config.optimized_compression_usage_counter_threshold, + expected_config.optimized_compression_usage_counter_threshold + ); + assert_eq!(config.cleanup_interval, expected_config.cleanup_interval); + assert_eq!( + config.optimizing_compression_task_timeout, + expected_config.optimizing_compression_task_timeout + ); + assert_eq!( + config.allowed_clock_drift_for_files_from_future, + expected_config.allowed_clock_drift_for_files_from_future + ); + assert_eq!( + config.file_count_soft_limit, + expected_config.file_count_soft_limit + ); + assert_eq!( + config.files_total_size_soft_limit, + expected_config.files_total_size_soft_limit + ); + assert_eq!( + config.file_count_limit_percent_if_deleting, + expected_config.file_count_limit_percent_if_deleting + ); + assert_eq!( + config.files_total_size_limit_percent_if_deleting, + expected_config.files_total_size_limit_percent_if_deleting + ); +} + +#[test] +fn test_builder_all_settings() { + let (_td, cd, _cp) = test_prolog(); + + let mut conf = CacheConfig::new(); + conf.with_directory(&cd) + .with_worker_event_queue_size(0x10) + .with_baseline_compression_level(3) + .with_optimized_compression_level(20) + .with_optimized_compression_usage_counter_threshold(0x100) + .with_cleanup_interval(Duration::from_secs(60 * 60)) + .with_optimizing_compression_task_timeout(Duration::from_secs(30 * 60)) + .with_allowed_clock_drift_for_files_from_future(Duration::from_secs(60 * 60 * 24)) + .with_file_count_soft_limit(0x10_000) + .with_files_total_size_soft_limit(512 * (1u64 << 20)) + .with_file_count_limit_percent_if_deleting(70) + .with_files_total_size_limit_percent_if_deleting(70); + conf.validate().expect("validation failed"); + check_conf(&conf, &cd); + + fn check_conf(conf: &CacheConfig, cd: &PathBuf) { + 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); + } +} diff --git a/crates/cache/src/lib.rs b/crates/cache/src/lib.rs index 88dbcfd953e1..bdf34e41a987 100644 --- a/crates/cache/src/lib.rs +++ b/crates/cache/src/lib.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use base64::Engine; use log::{debug, trace, warn}; use serde::{Deserialize, Serialize}; @@ -6,6 +7,9 @@ use std::hash::Hash; use std::hash::Hasher; use std::io::Write; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; +use std::sync::Arc; +use std::time::Duration; use std::{fs, io}; #[macro_use] // for tests @@ -15,31 +19,133 @@ mod worker; pub use config::{create_new_config, CacheConfig}; use worker::Worker; +/// Global configuration for how the cache is managed +#[derive(Debug, Clone)] +pub struct Cache { + config: CacheConfig, + worker: Worker, + state: Arc, +} + +macro_rules! generate_config_setting_getter { + ($setting:ident: $setting_type:ty) => { + /// Returns `$setting`. + /// + /// Panics if the cache is disabled. + pub fn $setting(&self) -> $setting_type { + self.config.$setting() + } + }; +} + +impl Cache { + /// Builds a [`Cache`] from the configuration and spawns the cache worker. + /// + /// If you want to load the cache configuration from a file, use [`CacheConfig::from_file`]. + /// You can call [`CacheConfig::new`] for the default configuration. + /// + /// # Errors + /// Returns an error if the configuration is invalid. + pub fn new(mut config: CacheConfig) -> Result { + config.validate()?; + Ok(Self { + worker: Worker::start_new(&config), + config, + state: Default::default(), + }) + } + + /// Loads cache configuration specified at `path`. + /// + /// This method will read the file specified by `path` on the filesystem and + /// attempt to load cache configuration from it. This method can also fail + /// due to I/O errors, misconfiguration, syntax errors, etc. For expected + /// syntax in the configuration file see the [documentation online][docs]. + /// + /// Passing in `None` loads cache configuration from the system default path. + /// This is located, for example, on Unix at `$HOME/.config/wasmtime/config.toml` + /// and is typically created with the `wasmtime config new` command. + /// + /// # Errors + /// + /// This method can fail due to any error that happens when loading the file + /// pointed to by `path` and attempting to load the cache configuration. + /// + /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html + pub fn from_file(path: Option<&Path>) -> Result { + let config = CacheConfig::from_file(path)?; + Self::new(config) + } + + generate_config_setting_getter!(worker_event_queue_size: u64); + generate_config_setting_getter!(baseline_compression_level: i32); + generate_config_setting_getter!(optimized_compression_level: i32); + generate_config_setting_getter!(optimized_compression_usage_counter_threshold: u64); + generate_config_setting_getter!(cleanup_interval: Duration); + generate_config_setting_getter!(optimizing_compression_task_timeout: Duration); + generate_config_setting_getter!(allowed_clock_drift_for_files_from_future: Duration); + generate_config_setting_getter!(file_count_soft_limit: u64); + generate_config_setting_getter!(files_total_size_soft_limit: u64); + generate_config_setting_getter!(file_count_limit_percent_if_deleting: u8); + generate_config_setting_getter!(files_total_size_limit_percent_if_deleting: u8); + + /// Returns path to the cache directory. + /// + /// Panics if the cache directory is not set. + pub fn directory(&self) -> &PathBuf { + &self.config.directory() + } + + #[cfg(test)] + fn worker(&self) -> &Worker { + &self.worker + } + + /// Returns the number of cache hits seen so far + pub fn cache_hits(&self) -> usize { + self.state.hits.load(SeqCst) + } + + /// Returns the number of cache misses seen so far + pub fn cache_misses(&self) -> usize { + self.state.misses.load(SeqCst) + } + + pub(crate) fn on_cache_get_async(&self, path: impl AsRef) { + self.state.hits.fetch_add(1, SeqCst); + self.worker.on_cache_get_async(path) + } + + pub(crate) fn on_cache_update_async(&self, path: impl AsRef) { + self.state.misses.fetch_add(1, SeqCst); + self.worker.on_cache_update_async(path) + } +} + +#[derive(Default, Debug)] +struct CacheState { + hits: AtomicUsize, + misses: AtomicUsize, +} + /// Module level cache entry. -pub struct ModuleCacheEntry<'config>(Option>); +pub struct ModuleCacheEntry<'cache>(Option>); -struct ModuleCacheEntryInner<'config> { +struct ModuleCacheEntryInner<'cache> { root_path: PathBuf, - cache_config: &'config CacheConfig, + cache: &'cache Cache, } struct Sha256Hasher(Sha256); -impl<'config> ModuleCacheEntry<'config> { +impl<'cache> ModuleCacheEntry<'cache> { /// Create the cache entry. - pub fn new(compiler_name: &str, cache_config: &'config CacheConfig) -> Self { - if cache_config.enabled() { - Self(Some(ModuleCacheEntryInner::new( - compiler_name, - cache_config, - ))) - } else { - Self(None) - } + pub fn new(compiler_name: &str, cache: Option<&'cache Cache>) -> Self { + Self(cache.map(|cache| ModuleCacheEntryInner::new(compiler_name, cache))) } #[cfg(test)] - fn from_inner(inner: ModuleCacheEntryInner<'config>) -> Self { + fn from_inner(inner: ModuleCacheEntryInner<'cache>) -> Self { Self(Some(inner)) } @@ -92,7 +198,7 @@ impl<'config> ModuleCacheEntry<'config> { if let Some(cached_val) = inner.get_data(&hash) { if let Some(val) = deserialize(state, cached_val) { let mod_cache_path = inner.root_path.join(&hash); - inner.cache_config.on_cache_get_async(&mod_cache_path); // call on success + inner.cache.on_cache_get_async(&mod_cache_path); // call on success return Ok(val); } } @@ -100,15 +206,15 @@ impl<'config> ModuleCacheEntry<'config> { if let Some(bytes) = serialize(state, &val_to_cache) { if inner.update_data(&hash, &bytes).is_some() { let mod_cache_path = inner.root_path.join(&hash); - inner.cache_config.on_cache_update_async(&mod_cache_path); // call on success + inner.cache.on_cache_update_async(&mod_cache_path); // call on success } } Ok(val_to_cache) } } -impl<'config> ModuleCacheEntryInner<'config> { - fn new(compiler_name: &str, cache_config: &'config CacheConfig) -> Self { +impl<'cache> ModuleCacheEntryInner<'cache> { + fn new(compiler_name: &str, cache: &'cache Cache) -> Self { // If debug assertions are enabled then assume that we're some sort of // local build. We don't want local builds to stomp over caches between // builds, so just use a separate cache directory based on the mtime of @@ -142,12 +248,9 @@ impl<'config> ModuleCacheEntryInner<'config> { comp_ver = env!("GIT_REV"), ) }; - let root_path = cache_config.directory().join("modules").join(compiler_dir); + let root_path = cache.directory().join("modules").join(compiler_dir); - Self { - root_path, - cache_config, - } + Self { root_path, cache } } fn get_data(&self, hash: &str) -> Option> { @@ -165,7 +268,7 @@ impl<'config> ModuleCacheEntryInner<'config> { trace!("update_data() for path: {}", mod_cache_path.display()); let compressed_data = zstd::encode_all( &serialized_data[..], - self.cache_config.baseline_compression_level(), + self.cache.baseline_compression_level(), ) .map_err(|err| warn!("Failed to compress cached code: {}", err)) .ok()?; diff --git a/crates/cache/src/tests.rs b/crates/cache/src/tests.rs index f901a009cd1f..a8a8c9169b7e 100644 --- a/crates/cache/src/tests.rs +++ b/crates/cache/src/tests.rs @@ -13,7 +13,6 @@ fn test_cache_init() { let baseline_compression_level = 4; let config_content = format!( "[cache]\n\ - enabled = true\n\ directory = '{}'\n\ baseline-compression-level = {}\n", cache_dir.display(), @@ -23,8 +22,6 @@ fn test_cache_init() { let cache_config = CacheConfig::from_file(Some(&config_path)).unwrap(); - // test if we can use config - assert!(cache_config.enabled()); // assumption: config init creates cache directory and returns canonicalized path assert_eq!( *cache_config.directory(), @@ -36,7 +33,10 @@ fn test_cache_init() { ); // test if we can use worker - cache_config.worker().on_cache_update_async(config_path); + Cache::new(cache_config) + .unwrap() + .worker() + .on_cache_update_async(config_path); } #[test] @@ -45,12 +45,11 @@ fn test_write_read_cache() { 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 cache = Cache::new(cache_config.clone()).unwrap(); // assumption: config load creates cache directory and returns canonicalized path assert_eq!( @@ -61,8 +60,8 @@ fn test_write_read_cache() { let compiler1 = "test-1"; let compiler2 = "test-2"; - let entry1 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(compiler1, &cache_config)); - let entry2 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(compiler2, &cache_config)); + let entry1 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(compiler1, &cache)); + let entry2 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(compiler2, &cache)); entry1.get_data::<_, i32, i32>(1, |_| Ok(100)).unwrap(); entry1.get_data::<_, i32, i32>(1, |_| panic!()).unwrap(); diff --git a/crates/cache/src/worker/tests.rs b/crates/cache/src/worker/tests.rs index 7392962d77d0..2d11366e7bb8 100644 --- a/crates/cache/src/worker/tests.rs +++ b/crates/cache/src/worker/tests.rs @@ -14,11 +14,9 @@ fn test_on_get_create_stats_file() { 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); let mod_file = cache_dir.join("some-mod"); @@ -41,12 +39,10 @@ fn test_on_get_update_usage_counter() { 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); let mod_file = cache_dir.join("some-mod"); @@ -75,7 +71,6 @@ fn test_on_get_recompress_no_mod_file() { 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\ @@ -83,7 +78,6 @@ fn test_on_get_recompress_no_mod_file() { optimized-compression-usage-counter-threshold = '256'", cache_dir ); - assert!(cache_config.enabled()); let worker = Worker::start_new(&cache_config); let mod_file = cache_dir.join("some-mod"); @@ -117,7 +111,6 @@ fn test_on_get_recompress_with_mod_file() { 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\ @@ -125,7 +118,6 @@ fn test_on_get_recompress_with_mod_file() { optimized-compression-usage-counter-threshold = '256'", cache_dir ); - assert!(cache_config.enabled()); let worker = Worker::start_new(&cache_config); let mod_file = cache_dir.join("some-mod"); @@ -192,7 +184,6 @@ fn test_on_get_recompress_lock() { 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\ @@ -202,7 +193,6 @@ fn test_on_get_recompress_lock() { allowed-clock-drift-for-files-from-future = '1d'", cache_dir ); - assert!(cache_config.enabled()); let worker = Worker::start_new(&cache_config); let mod_file = cache_dir.join("some-mod"); @@ -262,7 +252,6 @@ fn test_on_update_fresh_stats_file() { 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\ @@ -270,7 +259,6 @@ fn test_on_update_fresh_stats_file() { cleanup-interval = '1h'", cache_dir ); - assert!(cache_config.enabled()); let worker = Worker::start_new(&cache_config); let mod_file = cache_dir.join("some-mod"); @@ -311,7 +299,6 @@ fn test_on_update_cleanup_limits_trash_locks() { 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\ @@ -324,7 +311,6 @@ fn test_on_update_cleanup_limits_trash_locks() { ", cache_dir ); - assert!(cache_config.enabled()); let worker = Worker::start_new(&cache_config); let content_1k = "a".repeat(1_000); let content_10k = "a".repeat(10_000); @@ -452,7 +438,6 @@ fn test_on_update_cleanup_lru_policy() { 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\ @@ -461,7 +446,6 @@ fn test_on_update_cleanup_lru_policy() { files-total-size-limit-percent-if-deleting = '70%'", cache_dir ); - assert!(cache_config.enabled()); let worker = Worker::start_new(&cache_config); let content_1k = "a".repeat(1_000); let content_5k = "a".repeat(5_000); @@ -584,7 +568,6 @@ fn test_on_update_cleanup_future_files() { 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\ @@ -594,7 +577,6 @@ fn test_on_update_cleanup_future_files() { files-total-size-limit-percent-if-deleting = '70%'", cache_dir ); - assert!(cache_config.enabled()); let worker = Worker::start_new(&cache_config); let content_1k = "a".repeat(1_000); @@ -692,14 +674,12 @@ fn test_on_update_cleanup_self_lock() { 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); let mod_file = cache_dir.join("some-mod"); diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 6d90e08185f0..d61db2d55369 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -735,14 +735,12 @@ impl CommonOptions { #[cfg(feature = "cache")] if self.codegen.cache != Some(false) { - match &self.codegen.cache_config { - Some(path) => { - config.cache_config_load(path)?; - } - None => { - config.cache_config_load_default()?; - } - } + use wasmtime::Cache; + let cache = match &self.codegen.cache_config { + Some(path) => Cache::from_file(Some(Path::new(path)))?, + None => Cache::from_file(None)?, + }; + config.cache(Some(cache)); } #[cfg(not(feature = "cache"))] if self.codegen.cache == Some(true) { diff --git a/crates/wasmtime/src/compile/runtime.rs b/crates/wasmtime/src/compile/runtime.rs index d1462e9e3de9..46be42a01729 100644 --- a/crates/wasmtime/src/compile/runtime.rs +++ b/crates/wasmtime/src/compile/runtime.rs @@ -39,7 +39,7 @@ impl<'a> CodeBuilder<'a> { NotHashed(state), ); let (code, info_and_types) = - wasmtime_cache::ModuleCacheEntry::new("wasmtime", self.engine.cache_config()) + wasmtime_cache::ModuleCacheEntry::new("wasmtime", self.engine.cache()) .get_data_raw( &state, // Cache miss, compute the actual artifacts diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 094d5ee9c102..539e93d473ca 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -6,8 +6,6 @@ use core::str::FromStr; #[cfg(any(feature = "cache", feature = "cranelift", feature = "winch"))] use std::path::Path; use wasmparser::WasmFeatures; -#[cfg(feature = "cache")] -use wasmtime_cache::CacheConfig; use wasmtime_environ::{ConfigTunables, TripleExt, Tunables}; #[cfg(feature = "runtime")] @@ -28,6 +26,8 @@ use wasmtime_fiber::RuntimeFiberStackCreator; #[cfg(feature = "runtime")] pub use crate::runtime::code_memory::CustomCodeMemory; +#[cfg(feature = "cache")] +pub use wasmtime_cache::{Cache, CacheConfig}; #[cfg(all(feature = "incremental-cache", feature = "cranelift"))] pub use wasmtime_environ::CacheStore; @@ -128,7 +128,7 @@ pub struct Config { tunables: ConfigTunables, #[cfg(feature = "cache")] - pub(crate) cache_config: CacheConfig, + pub(crate) cache: Option, #[cfg(feature = "runtime")] pub(crate) mem_creator: Option>, #[cfg(feature = "runtime")] @@ -231,7 +231,7 @@ impl Config { #[cfg(feature = "gc")] collector: Collector::default(), #[cfg(feature = "cache")] - cache_config: CacheConfig::new_cache_disabled(), + cache: None, profiling_strategy: ProfilingStrategy::None, #[cfg(feature = "runtime")] mem_creator: None, @@ -1369,71 +1369,25 @@ impl Config { self } - /// Loads cache configuration specified at `path`. - /// - /// This method will read the file specified by `path` on the filesystem and - /// attempt to load cache configuration from it. This method can also fail - /// due to I/O errors, misconfiguration, syntax errors, etc. For expected - /// syntax in the configuration file see the [documentation online][docs]. - /// - /// By default cache configuration is not enabled or loaded. + /// Set a custom [`Cache`]. /// - /// This method is only available when the `cache` feature of this crate is - /// enabled. - /// - /// # Errors - /// - /// This method can fail due to any error that happens when loading the file - /// pointed to by `path` and attempting to load the cache configuration. - /// - /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html - #[cfg(feature = "cache")] - pub fn cache_config_load(&mut self, path: impl AsRef) -> Result<&mut Self> { - self.cache_config = CacheConfig::from_file(Some(path.as_ref()))?; - Ok(self) - } - - /// Disable caching. + /// To load a cache from a file, use [`Cache::from_file`]. Otherwise, you can create a new + /// cache config using [`CacheConfig::new`] and passing that to [`Cache::new`]. /// - /// Every call to [`Module::new(my_wasm)`][crate::Module::new] will - /// recompile `my_wasm`, even when it is unchanged. + /// If you want to disable the cache, you can call this method with `None`. /// - /// By default, new configs do not have caching enabled. This method is only - /// useful for disabling a previous cache configuration. + /// By default, new configs do not have caching enabled. + /// Every call to [`Module::new(my_wasm)`][crate::Module::new] will recompile `my_wasm`, + /// even when it is unchanged, unless an enabled `CacheConfig` is provided. /// /// This method is only available when the `cache` feature of this crate is /// enabled. - #[cfg(feature = "cache")] - pub fn disable_cache(&mut self) -> &mut Self { - self.cache_config = CacheConfig::new_cache_disabled(); - self - } - - /// Loads cache configuration from the system default path. - /// - /// This commit is the same as [`Config::cache_config_load`] except that it - /// does not take a path argument and instead loads the default - /// configuration present on the system. This is located, for example, on - /// Unix at `$HOME/.config/wasmtime/config.toml` and is typically created - /// with the `wasmtime config new` command. - /// - /// By default cache configuration is not enabled or loaded. - /// - /// This method is only available when the `cache` feature of this crate is - /// enabled. - /// - /// # Errors - /// - /// This method can fail due to any error that happens when loading the - /// default system configuration. Note that it is not an error if the - /// default config file does not exist, in which case the default settings - /// for an enabled cache are applied. /// /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html #[cfg(feature = "cache")] - pub fn cache_config_load_default(&mut self) -> Result<&mut Self> { - self.cache_config = CacheConfig::from_file(None)?; - Ok(self) + pub fn cache(&mut self, cache: Option) -> &mut Self { + self.cache = cache; + self } /// Sets a custom memory creator. diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index cfc7cb8416c8..be7677e5d34b 100644 --- a/crates/wasmtime/src/engine.rs +++ b/crates/wasmtime/src/engine.rs @@ -684,8 +684,8 @@ impl Engine { } #[cfg(all(feature = "cache", any(feature = "cranelift", feature = "winch")))] - pub(crate) fn cache_config(&self) -> &wasmtime_cache::CacheConfig { - &self.config().cache_config + pub(crate) fn cache(&self) -> Option<&wasmtime_cache::Cache> { + self.config().cache.as_ref() } pub(crate) fn signatures(&self) -> &TypeRegistry { diff --git a/crates/wasmtime/src/engine/serialization.rs b/crates/wasmtime/src/engine/serialization.rs index 0f4a7b08de9a..d3f62bdb3516 100644 --- a/crates/wasmtime/src/engine/serialization.rs +++ b/crates/wasmtime/src/engine/serialization.rs @@ -446,7 +446,7 @@ impl Metadata<'_> { #[cfg(test)] mod test { use super::*; - use crate::{Config, Module, OptLevel}; + use crate::{Cache, Config, Module, OptLevel}; use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, @@ -656,7 +656,6 @@ Caused by: &format!( " [cache] - enabled = true directory = '{}' ", td.path().join("cache").display() @@ -664,46 +663,67 @@ Caused by: )?; let mut cfg = Config::new(); cfg.cranelift_opt_level(OptLevel::None) - .cache_config_load(&config_path)?; + .cache(Some(Cache::from_file(Some(&config_path))?)); let engine = Engine::new(&cfg)?; Module::new(&engine, "(module (func))")?; - assert_eq!(engine.config().cache_config.cache_hits(), 0); - assert_eq!(engine.config().cache_config.cache_misses(), 1); + let cache_config = engine + .config() + .cache + .as_ref() + .expect("Missing cache config"); + assert_eq!(cache_config.cache_hits(), 0); + assert_eq!(cache_config.cache_misses(), 1); Module::new(&engine, "(module (func))")?; - assert_eq!(engine.config().cache_config.cache_hits(), 1); - assert_eq!(engine.config().cache_config.cache_misses(), 1); + assert_eq!(cache_config.cache_hits(), 1); + assert_eq!(cache_config.cache_misses(), 1); let mut cfg = Config::new(); cfg.cranelift_opt_level(OptLevel::Speed) - .cache_config_load(&config_path)?; + .cache(Some(Cache::from_file(Some(&config_path))?)); let engine = Engine::new(&cfg)?; + let cache_config = engine + .config() + .cache + .as_ref() + .expect("Missing cache config"); Module::new(&engine, "(module (func))")?; - assert_eq!(engine.config().cache_config.cache_hits(), 0); - assert_eq!(engine.config().cache_config.cache_misses(), 1); + assert_eq!(cache_config.cache_hits(), 0); + assert_eq!(cache_config.cache_misses(), 1); Module::new(&engine, "(module (func))")?; - assert_eq!(engine.config().cache_config.cache_hits(), 1); - assert_eq!(engine.config().cache_config.cache_misses(), 1); + assert_eq!(cache_config.cache_hits(), 1); + assert_eq!(cache_config.cache_misses(), 1); let mut cfg = Config::new(); cfg.cranelift_opt_level(OptLevel::SpeedAndSize) - .cache_config_load(&config_path)?; + .cache(Some(Cache::from_file(Some(&config_path))?)); let engine = Engine::new(&cfg)?; + let cache_config = engine + .config() + .cache + .as_ref() + .expect("Missing cache config"); Module::new(&engine, "(module (func))")?; - assert_eq!(engine.config().cache_config.cache_hits(), 0); - assert_eq!(engine.config().cache_config.cache_misses(), 1); + assert_eq!(cache_config.cache_hits(), 0); + assert_eq!(cache_config.cache_misses(), 1); Module::new(&engine, "(module (func))")?; - assert_eq!(engine.config().cache_config.cache_hits(), 1); - assert_eq!(engine.config().cache_config.cache_misses(), 1); + assert_eq!(cache_config.cache_hits(), 1); + assert_eq!(cache_config.cache_misses(), 1); let mut cfg = Config::new(); - cfg.debug_info(true).cache_config_load(&config_path)?; + cfg.debug_info(true) + .cache(Some(Cache::from_file(Some(&config_path))?)); let engine = Engine::new(&cfg)?; + let cache_config = engine + .config() + .cache + .as_ref() + .expect("Missing cache config"); Module::new(&engine, "(module (func))")?; - assert_eq!(engine.config().cache_config.cache_hits(), 0); - assert_eq!(engine.config().cache_config.cache_misses(), 1); + assert_eq!(cache_config.cache_hits(), 0); + assert_eq!(cache_config.cache_misses(), 1); Module::new(&engine, "(module (func))")?; - assert_eq!(engine.config().cache_config.cache_hits(), 1); - assert_eq!(engine.config().cache_config.cache_misses(), 1); + assert_eq!(cache_config.cache_hits(), 1); + assert_eq!(cache_config.cache_misses(), 1); Ok(()) } @@ -764,21 +784,25 @@ Caused by: &format!( " [cache] - enabled = true directory = '{}' ", td.path().join("cache").display() ), )?; let mut cfg = Config::new(); - cfg.cache_config_load(&config_path)?; + cfg.cache(Some(Cache::from_file(Some(&config_path))?)); let engine = Engine::new(&cfg)?; + let cache_config = engine + .config() + .cache + .as_ref() + .expect("Missing cache config"); Component::new(&engine, "(component (core module (func)))")?; - assert_eq!(engine.config().cache_config.cache_hits(), 0); - assert_eq!(engine.config().cache_config.cache_misses(), 1); + assert_eq!(cache_config.cache_hits(), 0); + assert_eq!(cache_config.cache_misses(), 1); Component::new(&engine, "(component (core module (func)))")?; - assert_eq!(engine.config().cache_config.cache_hits(), 1); - assert_eq!(engine.config().cache_config.cache_misses(), 1); + assert_eq!(cache_config.cache_hits(), 1); + assert_eq!(cache_config.cache_misses(), 1); Ok(()) } diff --git a/docs/cli-cache.md b/docs/cli-cache.md index f8c6622971e3..72cb2886b947 100644 --- a/docs/cli-cache.md +++ b/docs/cli-cache.md @@ -18,7 +18,6 @@ Wasmtime assumes all the options are in the `cache` section. Example config: ```toml [cache] -enabled = true directory = "/nfs-share/wasmtime-cache/" cleanup-interval = "30m" files-total-size-soft-limit = "1Gi" diff --git a/examples/fast_compilation.rs b/examples/fast_compilation.rs index e6cf4c0e240d..eae3887a69a9 100644 --- a/examples/fast_compilation.rs +++ b/examples/fast_compilation.rs @@ -3,14 +3,14 @@ //! If your application design is compatible with pre-compiling Wasm programs, //! prefer doing that. -use wasmtime::{Config, Engine, Result, Strategy}; +use wasmtime::{Cache, Config, Engine, Result, Strategy}; fn main() -> Result<()> { let mut config = Config::new(); // Enable the compilation cache, using the default cache configuration // settings. - config.cache_config_load_default()?; + config.cache(Some(Cache::from_file(None)?)); // Enable Winch, Wasmtime's baseline compiler. config.strategy(Strategy::Winch); diff --git a/src/commands/explore.rs b/src/commands/explore.rs index 9fa9f3bdd321..f0cf2f155eaa 100644 --- a/src/commands/explore.rs +++ b/src/commands/explore.rs @@ -51,7 +51,7 @@ impl ExploreCommand { let clif_dir = if let Some(Strategy::Cranelift) | None = self.common.codegen.compiler { let clif_dir = tempdir()?; config.emit_clif(clif_dir.path()); - config.disable_cache(); // cache does not emit clif + config.cache(None); // cache does not emit clif Some(clif_dir) } else { None