use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use freenet_stdlib::prelude::ContractKey;
use parking_lot::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct DiskUsageStats {
pub state_bytes: u64,
pub wasm_bytes: u64,
pub compile_cache_bytes: u64,
pub total_bytes: u64,
}
pub(crate) struct DiskUsageTracker {
state_bytes: AtomicU64,
wasm_bytes: AtomicU64,
compile_cache_bytes: AtomicU64,
seeded: AtomicBool,
state_sizes: Mutex<HashMap<ContractKey, u64>>,
contracts_dir: PathBuf,
compile_cache_dir: PathBuf,
}
impl DiskUsageTracker {
pub(crate) fn new(contracts_dir: PathBuf, compile_cache_dir: PathBuf) -> Self {
Self {
state_bytes: AtomicU64::new(0),
wasm_bytes: AtomicU64::new(0),
compile_cache_bytes: AtomicU64::new(0),
seeded: AtomicBool::new(false),
state_sizes: Mutex::new(HashMap::new()),
contracts_dir,
compile_cache_dir,
}
}
pub(crate) fn is_seeded(&self) -> bool {
self.seeded.load(Ordering::Acquire)
}
#[allow(dead_code)]
pub(crate) fn total_bytes(&self) -> u64 {
self.state_bytes
.load(Ordering::Relaxed)
.saturating_add(self.wasm_bytes.load(Ordering::Relaxed))
.saturating_add(self.compile_cache_bytes.load(Ordering::Relaxed))
}
pub(crate) fn stats(&self) -> DiskUsageStats {
let state_bytes = self.state_bytes.load(Ordering::Relaxed);
let wasm_bytes = self.wasm_bytes.load(Ordering::Relaxed);
let compile_cache_bytes = self.compile_cache_bytes.load(Ordering::Relaxed);
DiskUsageStats {
state_bytes,
wasm_bytes,
compile_cache_bytes,
total_bytes: state_bytes
.saturating_add(wasm_bytes)
.saturating_add(compile_cache_bytes),
}
}
pub(crate) fn seed<I>(&self, state_rows: I)
where
I: IntoIterator<Item = (ContractKey, u64)>,
{
let mut sizes = self.state_sizes.lock();
if self
.seeded
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return;
}
for (key, size) in state_rows {
match sizes.entry(key) {
std::collections::hash_map::Entry::Occupied(_) => {}
std::collections::hash_map::Entry::Vacant(v) => {
v.insert(size);
}
}
}
let total = sizes
.values()
.copied()
.fold(0u64, |acc, v| acc.saturating_add(v));
self.state_bytes.store(total, Ordering::Relaxed);
drop(sizes);
self.wasm_bytes
.store(du_walk_wasm(&self.contracts_dir), Ordering::Relaxed);
self.compile_cache_bytes
.store(du_walk(&self.compile_cache_dir), Ordering::Relaxed);
}
pub(crate) fn record_state_write(&self, key: &ContractKey, new_size: u64) {
let mut sizes = self.state_sizes.lock();
let old = sizes.insert(*key, new_size).unwrap_or(0);
if !self.seeded.load(Ordering::Acquire) {
return;
}
drop(sizes);
if new_size >= old {
self.state_bytes
.fetch_add(new_size - old, Ordering::Relaxed);
} else {
let dec = old - new_size;
let mut cur = self.state_bytes.load(Ordering::Relaxed);
loop {
let next = cur.saturating_sub(dec);
match self.state_bytes.compare_exchange_weak(
cur,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(observed) => cur = observed,
}
}
}
}
pub(crate) fn record_state_removed(&self, key: &ContractKey) {
let mut sizes = self.state_sizes.lock();
let removed = sizes.remove(key).unwrap_or(0);
if !self.seeded.load(Ordering::Acquire) {
return;
}
drop(sizes);
if removed == 0 {
return;
}
let mut cur = self.state_bytes.load(Ordering::Relaxed);
loop {
let next = cur.saturating_sub(removed);
match self.state_bytes.compare_exchange_weak(
cur,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(observed) => cur = observed,
}
}
}
pub(crate) fn refresh_wasm(&self) {
self.wasm_bytes
.store(du_walk_wasm(&self.contracts_dir), Ordering::Relaxed);
}
pub(crate) fn refresh_compile_cache(&self) {
self.compile_cache_bytes
.store(du_walk(&self.compile_cache_dir), Ordering::Relaxed);
}
}
fn du_walk(dir: &Path) -> u64 {
let mut total: u64 = 0;
let mut stack = vec![dir.to_path_buf()];
while let Some(path) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&path) else {
continue;
};
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_dir() {
stack.push(entry.path());
} else if file_type.is_file() {
if let Ok(meta) = entry.metadata() {
total = total.saturating_add(meta.len());
}
}
}
}
total
}
fn du_walk_wasm(dir: &Path) -> u64 {
let mut total: u64 = 0;
let mut stack = vec![dir.to_path_buf()];
while let Some(path) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&path) else {
continue;
};
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_dir() {
stack.push(entry.path());
} else if file_type.is_file() {
let p = entry.path();
if p.extension().and_then(|e| e.to_str()) == Some("wasm") {
if let Ok(meta) = entry.metadata() {
total = total.saturating_add(meta.len());
}
}
}
}
}
total
}
#[cfg(test)]
mod tests {
use super::*;
use freenet_stdlib::prelude::{CodeHash, ContractInstanceId, ContractKey};
use std::io::Write;
fn test_key(seed: u8) -> ContractKey {
let instance = ContractInstanceId::new([seed; 32]);
let code = CodeHash::new([seed; 32]);
ContractKey::from_id_and_code(instance, code)
}
fn tracker() -> DiskUsageTracker {
DiskUsageTracker::new(
PathBuf::from("/nonexistent/contracts"),
PathBuf::from("/nonexistent/cache"),
)
}
#[test]
fn seed_sums_state_rows_and_is_idempotent() {
let t = tracker();
assert!(!t.is_seeded());
t.seed([(test_key(1), 100), (test_key(2), 50)]);
assert!(t.is_seeded());
assert_eq!(t.stats().state_bytes, 150);
t.seed([(test_key(3), 999)]);
assert_eq!(t.stats().state_bytes, 150);
}
#[test]
fn put_new_contract_adds_full_size() {
let t = tracker();
t.seed(std::iter::empty());
t.record_state_write(&test_key(1), 200);
assert_eq!(t.stats().state_bytes, 200);
assert_eq!(t.total_bytes(), 200);
}
#[test]
fn update_grow_adds_delta() {
let t = tracker();
t.seed([(test_key(1), 100)]);
t.record_state_write(&test_key(1), 250);
assert_eq!(t.stats().state_bytes, 250); }
#[test]
fn update_shrink_subtracts_delta() {
let t = tracker();
t.seed([(test_key(1), 300)]);
t.record_state_write(&test_key(1), 100);
assert_eq!(t.stats().state_bytes, 100); }
#[test]
fn evict_removes_full_size() {
let t = tracker();
t.seed([(test_key(1), 100), (test_key(2), 40)]);
t.record_state_removed(&test_key(1));
assert_eq!(t.stats().state_bytes, 40);
}
#[test]
fn double_evict_floors_at_zero() {
let t = tracker();
t.seed([(test_key(1), 100)]);
t.record_state_removed(&test_key(1));
t.record_state_removed(&test_key(1));
assert_eq!(t.stats().state_bytes, 0);
}
#[test]
fn removal_never_drives_aggregate_negative() {
let t = tracker();
t.seed([(test_key(1), 10)]);
t.record_state_write(&test_key(2), 5); t.record_state_removed(&test_key(2)); t.record_state_removed(&test_key(1)); assert_eq!(t.stats().state_bytes, 0);
}
#[test]
fn wasm_dedup_rewalk_counts_each_blob_once() {
let dir = tempfile::tempdir().unwrap();
let contracts = dir.path().join("contracts");
std::fs::create_dir_all(contracts.join("local")).unwrap();
let mut a = std::fs::File::create(contracts.join("aaaa.wasm")).unwrap();
a.write_all(&[0u8; 100]).unwrap();
let mut b = std::fs::File::create(contracts.join("local").join("bbbb.wasm")).unwrap();
b.write_all(&[0u8; 40]).unwrap();
let mut junk = std::fs::File::create(contracts.join("index.db")).unwrap();
junk.write_all(&[0u8; 1000]).unwrap();
let t = DiskUsageTracker::new(contracts.clone(), dir.path().join("cache"));
t.seed(std::iter::empty());
assert_eq!(t.stats().wasm_bytes, 140);
t.refresh_wasm();
assert_eq!(t.stats().wasm_bytes, 140);
}
#[test]
fn unseeded_write_is_buffered_and_survives_seed() {
let t = tracker();
t.record_state_write(&test_key(1), 200);
assert!(!t.is_seeded());
t.seed([(test_key(2), 50)]);
assert_eq!(t.stats().state_bytes, 250);
assert_eq!(t.total_bytes(), 250);
}
#[test]
fn seed_prefers_concurrent_write_size_over_stale_snapshot() {
let t = tracker();
t.record_state_write(&test_key(1), 300); t.seed([(test_key(1), 100)]); assert_eq!(t.stats().state_bytes, 300);
}
#[test]
fn racing_write_against_seed_yields_exact_total() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering as O};
for _ in 0..200 {
let t = Arc::new(tracker());
let go = Arc::new(AtomicBool::new(false));
let writer = {
let t = Arc::clone(&t);
let go = Arc::clone(&go);
std::thread::spawn(move || {
while !go.load(O::Acquire) {
std::hint::spin_loop();
}
t.record_state_write(&test_key(9), 100);
t.record_state_write(&test_key(9), 500);
})
};
go.store(true, O::Release);
t.seed([(test_key(1), 100), (test_key(2), 50)]);
writer.join().unwrap();
assert_eq!(
t.stats().state_bytes,
650,
"seed/write race under-counted or double-counted"
);
}
}
#[test]
fn compile_cache_du_walk_seeds_and_refreshes() {
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("cache");
std::fs::create_dir_all(cache.join("sub")).unwrap();
let mut f = std::fs::File::create(cache.join("sub").join("mod.cache")).unwrap();
f.write_all(&[0u8; 512]).unwrap();
let t = DiskUsageTracker::new(dir.path().join("contracts"), cache.clone());
t.seed(std::iter::empty());
assert_eq!(t.stats().compile_cache_bytes, 512);
let mut g = std::fs::File::create(cache.join("mod2.cache")).unwrap();
g.write_all(&[0u8; 88]).unwrap();
t.refresh_compile_cache();
assert_eq!(t.stats().compile_cache_bytes, 600);
assert_eq!(t.total_bytes(), 600);
}
}