use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use memmap2::Mmap;
use rkyv::{Archive, Deserialize, Serialize};
use crate::error::{HoldError, Result};
use crate::state::{
CAP_TRACE_SAMPLE_SOURCE_LEGACY, FileState, GcMetrics, METADATA_VERSION, StateMetadata,
};
#[cfg(test)]
mod tests;
#[derive(Archive, Deserialize, Serialize, Debug, Clone)]
struct StateMetadataV2 {
pub version: u32,
pub files: HashMap<String, FileState>,
pub last_gc_mtime_nanos: Option<u128>,
}
impl From<StateMetadataV2> for StateMetadata {
fn from(v2: StateMetadataV2) -> Self {
StateMetadata {
version: v2.version,
files: v2.files,
last_gc_mtime_nanos: v2.last_gc_mtime_nanos,
gc_metrics: GcMetrics::default(),
}
}
}
#[derive(Archive, Deserialize, Serialize, Debug, Clone)]
struct StateMetadataV3 {
pub version: u32,
pub files: HashMap<String, FileState>,
pub last_gc_mtime_nanos: Option<u128>,
pub gc_metrics: GcMetricsV3,
}
#[derive(Archive, Deserialize, Serialize, Debug, Clone, PartialEq, Default)]
struct GcMetricsV3 {
pub runs: u32,
pub seed_initial_size: Option<u64>,
pub recent_initial_sizes: Vec<u64>,
pub recent_bytes_freed: Vec<u64>,
pub last_suggested_cap: Option<u64>,
}
impl From<StateMetadataV3> for StateMetadata {
fn from(v3: StateMetadataV3) -> Self {
StateMetadata {
version: v3.version,
files: v3.files,
last_gc_mtime_nanos: v3.last_gc_mtime_nanos,
gc_metrics: v3.gc_metrics.into(),
}
}
}
impl From<GcMetricsV3> for GcMetrics {
fn from(v3: GcMetricsV3) -> Self {
Self {
runs: v3.runs,
seed_initial_size: v3.seed_initial_size,
recent_initial_sizes: v3.recent_initial_sizes,
recent_bytes_freed: v3.recent_bytes_freed,
last_suggested_cap: v3.last_suggested_cap,
..Default::default()
}
}
}
#[derive(Archive, Deserialize, Serialize, Debug, Clone)]
struct StateMetadataV4 {
pub version: u32,
pub files: HashMap<String, FileState>,
pub last_gc_mtime_nanos: Option<u128>,
pub gc_metrics: GcMetricsV4,
}
#[derive(Archive, Deserialize, Serialize, Debug, Clone, PartialEq, Default)]
struct GcMetricsV4 {
pub runs: u32,
pub seed_initial_size: Option<u64>,
pub recent_initial_sizes: Vec<u64>,
pub recent_bytes_freed: Vec<u64>,
pub last_suggested_cap: Option<u64>,
pub recent_final_sizes: Vec<u64>,
pub last_cap_trace: Option<CapTraceV4>,
}
#[derive(Archive, Deserialize, Serialize, Debug, Clone, PartialEq, Default)]
struct CapTraceV4 {
pub baseline: u64,
pub growth_budget: u64,
pub observed_growth_pct: u64,
pub clamp_reason: String,
}
impl From<CapTraceV4> for crate::state::CapTrace {
fn from(v4: CapTraceV4) -> Self {
Self {
baseline: v4.baseline,
growth_budget: v4.growth_budget,
observed_growth_pct: v4.observed_growth_pct,
clamp_reason: v4.clamp_reason,
sample_source: CAP_TRACE_SAMPLE_SOURCE_LEGACY.to_string(),
sample_count: 0,
ignored_over_cap_sample_count: 0,
}
}
}
impl From<GcMetricsV4> for GcMetrics {
fn from(v4: GcMetricsV4) -> Self {
Self {
runs: v4.runs,
seed_initial_size: v4.seed_initial_size,
recent_initial_sizes: v4.recent_initial_sizes,
recent_bytes_freed: v4.recent_bytes_freed,
last_suggested_cap: v4.last_suggested_cap,
recent_final_sizes: v4.recent_final_sizes,
last_cap_trace: v4.last_cap_trace.map(Into::into),
..Default::default()
}
}
}
impl From<StateMetadataV4> for StateMetadata {
fn from(v4: StateMetadataV4) -> Self {
StateMetadata {
version: v4.version,
files: v4.files,
last_gc_mtime_nanos: v4.last_gc_mtime_nanos,
gc_metrics: v4.gc_metrics.into(),
}
}
}
pub fn load_metadata(metadata_path: &Path) -> Result<StateMetadata> {
match load_metadata_inner(metadata_path) {
Ok(metadata) => Ok(metadata),
Err(HoldError::DeserializationError { .. }) => {
eprintln!("⚠️ Detected incompatible metadata format from previous cargo-hold version");
eprintln!(" Automatically resetting metadata to use new format...");
if let Err(remove_err) = fs::remove_file(metadata_path) {
eprintln!(" Warning: Could not remove old metadata file: {remove_err}");
}
Ok(StateMetadata::new())
}
Err(other) => Err(other),
}
}
fn load_metadata_inner(metadata_path: &Path) -> Result<StateMetadata> {
if !metadata_path.exists() {
return Ok(StateMetadata::new());
}
let file = File::open(metadata_path).map_err(|source| HoldError::IoError {
path: metadata_path.to_path_buf(),
source,
})?;
let file_metadata = file.metadata().map_err(|source| HoldError::IoError {
path: metadata_path.to_path_buf(),
source,
})?;
if file_metadata.len() == 0 {
return Ok(StateMetadata::new());
}
let mmap = unsafe { Mmap::map(&file) }.map_err(|source| HoldError::IoError {
path: metadata_path.to_path_buf(),
source,
})?;
let metadata = deserialize_metadata(&mmap[..])?;
if metadata.version > METADATA_VERSION {
return Err(HoldError::ConfigError(format!(
"Metadata version {} is newer than supported version {}. Please update cargo-hold.",
metadata.version, METADATA_VERSION
)));
}
let metadata = if metadata.version < METADATA_VERSION {
migrate_metadata(metadata)?
} else {
metadata
};
Ok(metadata)
}
fn migrate_metadata(mut metadata: StateMetadata) -> Result<StateMetadata> {
if metadata.version == 1 {
metadata.version = 2;
}
if metadata.version == 2 {
metadata.gc_metrics = Default::default();
metadata.version = 3;
}
if metadata.version == 3 {
metadata.gc_metrics.recent_final_sizes = Vec::new();
metadata.gc_metrics.last_cap_trace = None;
metadata.version = 4;
}
if metadata.version == 4 {
metadata.gc_metrics.recent_sizing_final_sizes =
healthy_sizing_finals_from_v4(&metadata.gc_metrics);
metadata.gc_metrics.recent_cap_overage_bytes = Vec::new();
metadata.version = 5;
}
Ok(metadata)
}
fn deserialize_metadata(bytes: &[u8]) -> Result<StateMetadata> {
match rkyv::from_bytes::<StateMetadata, rkyv::rancor::BoxedError>(bytes) {
Ok(metadata) => Ok(metadata),
Err(primary_err) => {
if let Ok(v4) = rkyv::from_bytes::<StateMetadataV4, rkyv::rancor::BoxedError>(bytes) {
return Ok(StateMetadata::from(v4));
}
if let Ok(v3) = rkyv::from_bytes::<StateMetadataV3, rkyv::rancor::BoxedError>(bytes) {
return Ok(StateMetadata::from(v3));
}
if let Ok(v2) = rkyv::from_bytes::<StateMetadataV2, rkyv::rancor::BoxedError>(bytes) {
return Ok(StateMetadata::from(v2));
}
Err(HoldError::DeserializationError(primary_err))
}
}
}
fn healthy_sizing_finals_from_v4(metrics: &GcMetrics) -> Vec<u64> {
let Some(cap) = metrics.last_suggested_cap else {
return Vec::new();
};
metrics
.recent_final_sizes
.iter()
.copied()
.filter(|final_size| *final_size <= cap)
.collect()
}
pub fn save_metadata(metadata: &StateMetadata, metadata_path: &Path) -> Result<()> {
if let Some(parent) = metadata_path.parent() {
fs::create_dir_all(parent)
.map_err(|source| HoldError::CreateMetadataDirError(parent.to_path_buf(), source))?;
}
let bytes = rkyv::to_bytes::<rkyv::rancor::BoxedError>(metadata)
.map_err(|e| HoldError::SerializationError(Box::new(e)))?;
let temp_path = metadata_path.with_extension("tmp");
let mut temp_file = File::create(&temp_path).map_err(|source| HoldError::IoError {
path: temp_path.clone(),
source,
})?;
temp_file
.write_all(&bytes)
.map_err(|source| HoldError::IoError {
path: temp_path.clone(),
source,
})?;
temp_file.sync_all().map_err(|source| HoldError::IoError {
path: temp_path.clone(),
source,
})?;
fs::rename(&temp_path, metadata_path).map_err(|source| HoldError::IoError {
path: metadata_path.to_path_buf(),
source,
})?;
Ok(())
}
pub fn clean_metadata(metadata_path: &Path) -> Result<()> {
if metadata_path.exists() {
fs::remove_file(metadata_path).map_err(|source| HoldError::IoError {
path: metadata_path.to_path_buf(),
source,
})?;
}
Ok(())
}