use std::fs;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tempfile::TempDir;
use super::*;
use crate::gc::auto_cap::{
GC_METRICS_WINDOW, MAX_GROWTH_FACTOR_PER_RUN_PCT, MAX_SHRINK_FACTOR_PER_RUN_PCT,
MIN_HEADROOM_BYTES, MIN_STEADY_HEADROOM_BYTES, record_auto_cap_outcome,
suggest_max_target_size,
};
use crate::metadata::{load_metadata, save_metadata};
use crate::state::{
AutoCapRun, CAP_TRACE_SAMPLE_SOURCE_HEALTHY, CAP_TRACE_SAMPLE_SOURCE_HELD,
CAP_TRACE_SAMPLE_SOURCE_POLICY_FLOOR, GcMetrics, METADATA_VERSION, StateMetadata,
};
fn setup_git_repo() -> TempDir {
let temp_dir = TempDir::new().unwrap();
let repo = git2::Repository::init(temp_dir.path()).unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "test content").unwrap();
let mut index = repo.index().unwrap();
index.add_path(Path::new("test.txt")).unwrap();
index.write().unwrap();
temp_dir
}
#[test]
fn test_stow_command() {
let temp_dir = setup_git_repo();
let metadata_path = temp_dir.path().join("test.metadata");
stow(&metadata_path, 0, false, temp_dir.path()).unwrap();
assert!(metadata_path.exists());
let metadata = load_metadata(&metadata_path).unwrap();
assert_eq!(metadata.len(), 1);
}
#[test]
fn test_stow_fails_when_tracked_file_is_missing() {
let temp_dir = setup_git_repo();
let metadata_path = temp_dir.path().join("test.metadata");
fs::remove_file(temp_dir.path().join("test.txt")).unwrap();
let err = stow(&metadata_path, 0, true, temp_dir.path()).unwrap_err();
assert!(matches!(
err,
HoldError::PartialFileProcessing {
failed: 1,
total: 1,
}
));
assert!(!metadata_path.exists());
}
#[test]
fn test_stow_from_subdirectory() {
let temp_dir = setup_git_repo();
let subdir = temp_dir.path().join("subdir");
fs::create_dir(&subdir).unwrap();
let metadata_path = temp_dir.path().join("test.metadata");
stow(&metadata_path, 0, false, &subdir).unwrap();
assert!(metadata_path.exists());
let metadata = load_metadata(&metadata_path).unwrap();
assert_eq!(metadata.len(), 1);
}
#[test]
fn test_salvage_from_subdirectory() {
let temp_dir = setup_git_repo();
let subdir = temp_dir.path().join("src");
fs::create_dir(&subdir).unwrap();
let metadata_path = temp_dir.path().join("test.metadata");
stow(&metadata_path, 0, false, temp_dir.path()).unwrap();
salvage(&metadata_path, 0, false, &subdir).unwrap();
}
#[test]
fn test_salvage_fails_when_tracked_file_is_missing() {
let temp_dir = setup_git_repo();
let metadata_path = temp_dir.path().join("test.metadata");
stow(&metadata_path, 0, true, temp_dir.path()).unwrap();
fs::remove_file(temp_dir.path().join("test.txt")).unwrap();
let err = salvage(&metadata_path, 0, true, temp_dir.path()).unwrap_err();
assert!(matches!(
err,
HoldError::PartialFileProcessing {
failed: 1,
total: 1,
}
));
}
#[test]
fn test_bilge_command() {
let temp_dir = setup_git_repo();
let metadata_path = temp_dir.path().join("test.metadata");
stow(&metadata_path, 0, false, temp_dir.path()).unwrap();
assert!(metadata_path.exists());
bilge(&metadata_path, 0, false).unwrap();
assert!(!metadata_path.exists());
}
#[test]
fn test_anchor_command() {
let temp_dir = setup_git_repo();
let metadata_path = temp_dir.path().join("test.metadata");
anchor(&metadata_path, 0, false, temp_dir.path()).unwrap();
assert!(metadata_path.exists());
let metadata = load_metadata(&metadata_path).unwrap();
assert_eq!(metadata.len(), 1);
}
#[test]
fn test_stow_propagates_future_metadata_error() {
let temp_dir = setup_git_repo();
let metadata_path = temp_dir.path().join("test.metadata");
let mut metadata = StateMetadata::new();
metadata.version = METADATA_VERSION + 1;
save_metadata(&metadata, &metadata_path).unwrap();
let err = stow(&metadata_path, 0, false, temp_dir.path()).unwrap_err();
assert!(matches!(err, HoldError::ConfigError(_)));
}
#[test]
fn test_stow_preserves_last_gc_timestamp_when_time_advances() {
let temp_dir = setup_git_repo();
let metadata_path = temp_dir.path().join("test.metadata");
let one_hour_ago = SystemTime::now() - Duration::from_secs(3600);
let expected_nanos = one_hour_ago.duration_since(UNIX_EPOCH).unwrap().as_nanos();
let mut seed = StateMetadata::new();
seed.last_gc_mtime_nanos = Some(expected_nanos);
save_metadata(&seed, &metadata_path).unwrap();
std::thread::sleep(Duration::from_millis(10));
stow(&metadata_path, 0, false, temp_dir.path()).unwrap();
let second_metadata = load_metadata(&metadata_path).unwrap();
let second_preservation = second_metadata
.last_gc_mtime_nanos
.expect("stow should keep last_gc_mtime_nanos set");
assert_eq!(second_preservation, expected_nanos);
}
#[test]
fn test_stow_preserves_gc_metrics() {
let temp_dir = setup_git_repo();
let metadata_path = temp_dir.path().join("test.metadata");
let mut existing = StateMetadata::new();
existing.gc_metrics = GcMetrics {
runs: 3,
recent_auto_cap_runs: vec![AutoCapRun {
cap: 456,
initial_size: 120,
final_size: 100,
bytes_freed: 20,
protected_artifact_bytes: 90,
eligible_artifact_bytes: 30,
retained_artifact_bytes: 100,
preserved_binary_bytes: 5,
unrecognized_bytes: 5,
}],
last_suggested_cap: Some(456),
last_cap_trace: Some(crate::state::CapTrace {
baseline: 100,
growth_budget: 20,
observed_growth_pct: 5,
clamp_reason: "deadband/hold".to_string(),
sample_source: CAP_TRACE_SAMPLE_SOURCE_HEALTHY.to_string(),
sample_count: 2,
ignored_over_cap_sample_count: 1,
policy_floor: 0,
policy_floor_sample_count: 0,
}),
};
save_metadata(&existing, &metadata_path).unwrap();
stow(&metadata_path, 0, false, temp_dir.path()).unwrap();
let reloaded = load_metadata(&metadata_path).unwrap();
assert_eq!(reloaded.gc_metrics, existing.gc_metrics);
}
fn make_profile(target: &Path) {
let profile = target.join("debug");
fs::create_dir_all(profile.join("build")).unwrap();
fs::create_dir_all(profile.join("deps")).unwrap();
fs::create_dir_all(profile.join(".fingerprint")).unwrap();
}
fn write_crate_artifact(target: &Path, name: &str, hash: &str, size: usize, mtime: SystemTime) {
let profile = target.join("debug");
let deps = profile.join("deps");
fs::create_dir_all(&deps).unwrap();
let artifact = deps.join(format!("lib{name}-{hash}.rlib"));
fs::write(&artifact, vec![0u8; size]).unwrap();
filetime::set_file_mtime(&artifact, filetime::FileTime::from_system_time(mtime)).unwrap();
let fingerprint = profile.join(format!(".fingerprint/lib{name}-{hash}"));
fs::create_dir_all(&fingerprint).unwrap();
let invoked = fingerprint.join("invoked.timestamp");
fs::write(&invoked, b"dummy").unwrap();
let filetime = filetime::FileTime::from_system_time(mtime);
filetime::set_file_mtime(&fingerprint, filetime).unwrap();
filetime::set_file_mtime(&invoked, filetime).unwrap();
}
#[test]
fn voyage_gc_cooldown_preserves_anchor_and_gc_history() {
for (name, previous_age_hours, interval, force, should_run) in [
("first run", None, Some(24), false, true),
("within cooldown", Some(1), Some(24), false, false),
("expired cooldown", Some(25), Some(24), false, true),
("force override", Some(1), Some(24), true, true),
("no interval", Some(1), None, false, true),
("zero interval", Some(1), Some(0), false, true),
("future timestamp", Some(-1), Some(24), false, false),
("largest interval", Some(1), Some(u64::MAX), false, false),
] {
let repo = setup_git_repo();
let target = repo.path().join("target");
make_profile(&target);
let path = target.join("cargo-hold.metadata");
stow(&path, 0, true, repo.path()).unwrap();
let mut before = load_metadata(&path).unwrap();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
before.last_gc_mtime_nanos = previous_age_hours
.map(|hours| (now as i128 - i128::from(hours) * 3_600_000_000_000) as u128);
before.gc_metrics.runs = 3;
before.gc_metrics.last_suggested_cap = Some(1024);
before.gc_metrics.recent_auto_cap_runs.push(AutoCapRun {
cap: 1024,
final_size: 512,
..Default::default()
});
save_metadata(&before, &path).unwrap();
let source = repo.path().join("test.txt");
let original_mtime = fs::metadata(&source).unwrap().modified().unwrap();
filetime::set_file_mtime(
&source,
filetime::FileTime::from_system_time(original_mtime + Duration::from_secs(60)),
)
.unwrap();
Voyage::builder()
.working_dir(repo.path())
.metadata_path(&path)
.target_dir(&target)
.gc_min_interval_hours(interval)
.force_gc(force)
.quiet(true)
.build()
.unwrap()
.run()
.unwrap();
let after = load_metadata(&path).unwrap();
assert_eq!(
fs::metadata(&source).unwrap().modified().unwrap(),
original_mtime,
"{name}"
);
if should_run {
assert!(after.last_gc_mtime_nanos.unwrap() >= now, "{name}");
assert_eq!(after.gc_metrics.runs, before.gc_metrics.runs + 1, "{name}");
} else {
assert_eq!(
after.last_gc_mtime_nanos, before.last_gc_mtime_nanos,
"{name}"
);
assert_eq!(after.gc_metrics, before.gc_metrics, "{name}");
}
}
}
#[test]
fn test_heave_records_last_gc_timestamp() {
let temp_dir = TempDir::new().unwrap();
let target_dir = temp_dir.path().join("target");
make_profile(&target_dir);
let metadata_path = temp_dir.path().join("cargo-hold.metadata");
let before = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
Heave::builder()
.target_dir(&target_dir)
.max_target_size(None)
.auto_max_target_size(false)
.metadata_path(&metadata_path)
.age_threshold_days(7)
.verbose(0)
.quiet(true)
.build()
.unwrap()
.heave()
.unwrap();
let after = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let reloaded = load_metadata(&metadata_path).unwrap();
let recorded = reloaded
.last_gc_mtime_nanos
.expect("heave should record last_gc_mtime_nanos");
assert!(
recorded >= before && recorded <= after,
"last_gc_mtime_nanos should reflect GC time"
);
}
#[test]
fn fresh_heave_records_current_auto_cap_model() {
let temp_dir = TempDir::new().unwrap();
let target_dir = temp_dir.path().join("target");
make_profile(&target_dir);
let metadata_path = target_dir.join("cargo-hold.metadata");
Heave::builder()
.target_dir(&target_dir)
.max_target_size(None)
.auto_max_target_size(true)
.metadata_path(&metadata_path)
.age_threshold_days(7)
.verbose(0)
.quiet(true)
.build()
.unwrap()
.heave()
.unwrap();
let metrics = load_metadata(&metadata_path).unwrap().gc_metrics;
assert_eq!(metrics.runs, 1);
assert_eq!(metrics.last_suggested_cap, Some(MIN_HEADROOM_BYTES));
assert_eq!(metrics.recent_auto_cap_runs.len(), 1);
assert_eq!(metrics.recent_auto_cap_runs[0].final_size, 0);
}
#[test]
fn heave_auto_cap_can_be_disabled() {
let temp_dir = TempDir::new().unwrap();
let target_dir = temp_dir.path().join("target");
make_profile(&target_dir);
let metadata_path = temp_dir.path().join("cargo-hold.metadata");
Heave::builder()
.target_dir(&target_dir)
.max_target_size(None)
.auto_max_target_size(false)
.metadata_path(&metadata_path)
.age_threshold_days(7)
.verbose(0)
.quiet(true)
.build()
.unwrap()
.heave()
.unwrap();
let metrics = load_metadata(&metadata_path).unwrap().gc_metrics;
assert!(metrics.last_suggested_cap.is_none());
assert!(metrics.recent_auto_cap_runs.is_empty());
}
#[test]
fn heave_records_why_previous_build_makes_cap_unattainable() {
let temp_dir = TempDir::new().unwrap();
let target_dir = temp_dir.path().join("target");
make_profile(&target_dir);
let metadata_path = temp_dir.path().join("cargo-hold.metadata");
let previous_gc = SystemTime::now() - Duration::from_secs(1);
let mut metadata = StateMetadata::new();
metadata.last_gc_mtime_nanos = Some(previous_gc.duration_since(UNIX_EPOCH).unwrap().as_nanos());
metadata.gc_metrics.last_suggested_cap = Some(1024);
metadata.gc_metrics.recent_auto_cap_runs = vec![AutoCapRun {
cap: 1024,
initial_size: 1024,
final_size: 1024,
retained_artifact_bytes: 1024,
..Default::default()
}];
save_metadata(&metadata, &metadata_path).unwrap();
write_crate_artifact(
&target_dir,
"preserved",
"1234567890abcd12",
32 * 1024,
SystemTime::now(),
);
write_crate_artifact(
&target_dir,
"eligible",
"abcdef1234567890",
16 * 1024,
SystemTime::now() - Duration::from_secs(24 * 60 * 60),
);
fs::write(
target_dir.join("debug/unrecognized.bin"),
vec![0u8; 8 * 1024],
)
.unwrap();
write_preserved_binary(&target_dir.join("debug"), 4 * 1024);
Heave::builder()
.target_dir(&target_dir)
.max_target_size(None)
.auto_max_target_size(true)
.metadata_path(&metadata_path)
.age_threshold_days(7)
.verbose(0)
.quiet(true)
.build()
.unwrap()
.heave()
.unwrap();
let metrics = load_metadata(&metadata_path).unwrap().gc_metrics;
let run = metrics.recent_auto_cap_runs.last().unwrap();
assert!(run.protected_artifact_bytes >= 32 * 1024);
assert!(run.eligible_artifact_bytes >= 16 * 1024);
assert!(run.bytes_freed >= 16 * 1024);
assert!(run.retained_artifact_bytes >= run.protected_artifact_bytes);
assert!(run.preserved_binary_bytes >= 4 * 1024);
assert!(run.unrecognized_bytes >= 8 * 1024);
assert!(run.final_size > run.cap);
assert!(run.policy_floor() > run.cap);
}
fn write_preserved_binary(profile_dir: &Path, size: usize) {
#[cfg(unix)]
let path = profile_dir.join("preserved-runner");
#[cfg(not(unix))]
let path = profile_dir.join("preserved-runner.exe");
fs::write(&path, vec![0u8; size]).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = fs::metadata(&path).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(path, permissions).unwrap();
}
}
#[derive(Clone, Copy, Default)]
struct SyntheticTarget {
protected_artifacts: u64,
preserved_binaries: u64,
eligible_artifacts: u64,
unrecognized: u64,
}
impl SyntheticTarget {
fn total(self) -> u64 {
self.protected_artifacts
.saturating_add(self.preserved_binaries)
.saturating_add(self.eligible_artifacts)
.saturating_add(self.unrecognized)
}
}
fn synthetic_voyage(metrics: &mut GcMetrics, target: SyntheticTarget) -> AutoCapRun {
let initial_size = target.total();
let (cap, trace) = suggest_max_target_size(metrics, Some(initial_size)).unwrap();
let bytes_freed = initial_size
.saturating_sub(cap)
.min(target.eligible_artifacts);
let run = AutoCapRun {
cap,
initial_size,
final_size: initial_size - bytes_freed,
bytes_freed,
protected_artifact_bytes: target.protected_artifacts,
eligible_artifact_bytes: target.eligible_artifacts,
retained_artifact_bytes: target
.protected_artifacts
.saturating_add(target.eligible_artifacts.saturating_sub(bytes_freed)),
preserved_binary_bytes: target.preserved_binaries,
unrecognized_bytes: target.unrecognized,
};
metrics.last_suggested_cap = Some(cap);
metrics.last_cap_trace = Some(trace);
record_auto_cap_outcome(metrics, run.clone());
run
}
fn metrics_with_healthy_run(cap: u64, final_size: u64) -> GcMetrics {
GcMetrics {
last_suggested_cap: Some(cap),
recent_auto_cap_runs: vec![AutoCapRun {
cap,
initial_size: final_size,
final_size,
retained_artifact_bytes: final_size,
..Default::default()
}],
..Default::default()
}
}
#[test]
fn metadata_only_then_large_fallback_build_recovers_to_protected_working_set() {
let mib = 1024 * 1024;
let gib = 1024 * mib;
let mut metrics = GcMetrics::default();
let cold = synthetic_voyage(
&mut metrics,
SyntheticTarget {
unrecognized: mib,
..Default::default()
},
);
assert_eq!(cold.cap, MIN_HEADROOM_BYTES + mib);
assert_eq!(cold.bytes_freed, 0);
let first_restore = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 10 * gib + 9 * gib / 10,
eligible_artifacts: 6 * gib + gib / 2,
unrecognized: 2 * gib + gib / 2,
..Default::default()
},
);
assert!(first_restore.protected_artifact_bytes > first_restore.cap);
assert_eq!(first_restore.bytes_freed, 6 * gib + gib / 2);
let second_restore = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 17 * gib + 2 * gib / 5,
unrecognized: 2 * gib + gib / 2,
..Default::default()
},
);
assert_eq!(second_restore.cap, first_restore.cap);
assert!(second_restore.protected_artifact_bytes > second_restore.cap);
let recovered = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 17 * gib + 2 * gib / 5,
unrecognized: 2 * gib + gib / 2,
..Default::default()
},
);
assert_eq!(
metrics.last_cap_trace.as_ref().unwrap().sample_source,
CAP_TRACE_SAMPLE_SOURCE_POLICY_FLOOR
);
assert_eq!(
recovered.cap,
(10 * gib + 9 * gib / 10) + MIN_HEADROOM_BYTES
);
assert_eq!(recovered.bytes_freed, 0);
let recovered_again = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 17 * gib + 2 * gib / 5,
unrecognized: 2 * gib + gib / 2,
..Default::default()
},
);
assert_eq!(
recovered_again.cap,
(17 * gib + 2 * gib / 5) + MIN_HEADROOM_BYTES
);
assert_eq!(recovered_again.bytes_freed, 0);
for _ in 0..=GC_METRICS_WINDOW {
let run = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 17 * gib + 2 * gib / 5,
unrecognized: 2 * gib + gib / 2,
..Default::default()
},
);
assert_eq!(run.bytes_freed, 0);
assert_eq!(run.cap, recovered_again.cap);
}
assert_eq!(metrics.recent_auto_cap_runs.len(), GC_METRICS_WINDOW);
assert!(
metrics
.recent_auto_cap_runs
.iter()
.all(|run| run.initial_size > gib)
);
}
#[test]
fn full_window_of_protected_overages_cannot_keep_an_ancient_healthy_sample_alive() {
let gib = 1024 * 1024 * 1024;
let tiny_cap = 257 * 1024 * 1024;
let mut metrics = GcMetrics {
last_suggested_cap: Some(tiny_cap),
recent_auto_cap_runs: vec![AutoCapRun {
cap: tiny_cap,
initial_size: 1024 * 1024,
final_size: 1024 * 1024,
unrecognized_bytes: 1024 * 1024,
..Default::default()
}],
..Default::default()
};
for _ in 0..(GC_METRICS_WINDOW + 5) {
record_auto_cap_outcome(
&mut metrics,
AutoCapRun {
cap: tiny_cap,
initial_size: 20 * gib,
final_size: 13 * gib,
bytes_freed: 7 * gib,
protected_artifact_bytes: 11 * gib,
eligible_artifact_bytes: 7 * gib,
retained_artifact_bytes: 11 * gib,
unrecognized_bytes: 2 * gib,
..Default::default()
},
);
}
assert_eq!(metrics.recent_auto_cap_runs.len(), GC_METRICS_WINDOW);
assert!(
metrics
.recent_auto_cap_runs
.iter()
.all(|run| run.final_size > run.cap)
);
let (cap, trace) = suggest_max_target_size(&metrics, Some(20 * gib)).unwrap();
assert_eq!(cap, 11 * gib + MIN_HEADROOM_BYTES);
assert_eq!(trace.sample_source, CAP_TRACE_SAMPLE_SOURCE_POLICY_FLOOR);
assert_eq!(trace.sample_count, GC_METRICS_WINDOW as u32);
}
#[test]
fn repeated_unrecognized_256_gib_bloat_never_ratchets_cap() {
let gib = 1024 * 1024 * 1024;
let mut metrics = metrics_with_healthy_run(12 * gib, 10 * gib);
let mut held_cap = None;
for _ in 0..(GC_METRICS_WINDOW * 2) {
let run = synthetic_voyage(
&mut metrics,
SyntheticTarget {
unrecognized: 256 * gib,
..Default::default()
},
);
assert!(run.cap <= 12 * gib);
if let Some(expected) = held_cap {
assert_eq!(run.cap, expected);
assert_eq!(
metrics.last_cap_trace.as_ref().unwrap().sample_source,
CAP_TRACE_SAMPLE_SOURCE_HELD
);
} else {
held_cap = Some(run.cap);
}
assert_eq!(metrics.last_cap_trace.as_ref().unwrap().policy_floor, 0);
}
assert_eq!(metrics.recent_auto_cap_runs.len(), GC_METRICS_WINDOW);
}
#[test]
fn repeated_eligible_256_gib_bloat_only_moves_cap_through_normal_clamps() {
let gib = 1024 * 1024 * 1024;
let mut metrics = metrics_with_healthy_run(12 * gib, 10 * gib);
let mut previous_cap = 12 * gib;
let mut consecutive_holds = 0;
for _ in 0..(GC_METRICS_WINDOW * 3) {
let run = synthetic_voyage(
&mut metrics,
SyntheticTarget {
eligible_artifacts: 256 * gib,
..Default::default()
},
);
assert!(
run.cap
<= previous_cap.saturating_add(previous_cap * MAX_GROWTH_FACTOR_PER_RUN_PCT / 100)
);
assert_ne!(
metrics.last_cap_trace.as_ref().unwrap().sample_source,
CAP_TRACE_SAMPLE_SOURCE_POLICY_FLOOR
);
consecutive_holds = if run.cap == previous_cap {
consecutive_holds + 1
} else {
0
};
previous_cap = run.cap;
}
assert!(consecutive_holds >= GC_METRICS_WINDOW);
}
#[test]
fn isolated_policy_floor_spike_does_not_trigger_recovery() {
let gib = 1024 * 1024 * 1024;
let mut metrics = metrics_with_healthy_run(12 * gib, 10 * gib);
let spike = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 128 * gib,
preserved_binaries: 128 * gib,
..Default::default()
},
);
assert!(spike.cap <= 12 * gib);
let first_normal = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 10 * gib,
..Default::default()
},
);
assert_eq!(first_normal.cap, spike.cap);
assert_eq!(
metrics.last_cap_trace.as_ref().unwrap().sample_source,
CAP_TRACE_SAMPLE_SOURCE_HELD
);
let second_normal = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 10 * gib,
..Default::default()
},
);
assert!(second_normal.cap <= spike.cap);
assert_ne!(
metrics.last_cap_trace.as_ref().unwrap().sample_source,
CAP_TRACE_SAMPLE_SOURCE_POLICY_FLOOR
);
}
#[test]
fn preserved_binaries_contribute_to_confirmed_policy_floor() {
let gib = 1024 * 1024 * 1024;
let mut metrics = metrics_with_healthy_run(4 * gib, 4 * gib);
let working_set = SyntheticTarget {
protected_artifacts: 6 * gib,
preserved_binaries: 5 * gib,
..Default::default()
};
let first = synthetic_voyage(&mut metrics, working_set);
let second = synthetic_voyage(&mut metrics, working_set);
assert_eq!(first.cap, 4 * gib);
assert_eq!(second.cap, 4 * gib);
assert_eq!(second.policy_floor(), 11 * gib);
let recovered = synthetic_voyage(&mut metrics, working_set);
assert_eq!(recovered.cap, 11 * gib + MIN_HEADROOM_BYTES);
assert_eq!(
metrics.last_cap_trace.as_ref().unwrap().sample_source,
CAP_TRACE_SAMPLE_SOURCE_POLICY_FLOOR
);
assert_eq!(
metrics.last_cap_trace.as_ref().unwrap().policy_floor,
11 * gib
);
}
#[test]
fn stable_healthy_runs_do_not_oscillate_and_an_isolated_spike_does_not_stick() {
let gib = 1024 * 1024 * 1024;
let mut metrics = metrics_with_healthy_run(12 * gib, 10 * gib);
let mut previous_cap = 12 * gib;
for _ in 0..GC_METRICS_WINDOW {
let run = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 10 * gib,
..Default::default()
},
);
assert!(run.cap <= previous_cap);
assert!(previous_cap - run.cap <= previous_cap * MAX_SHRINK_FACTOR_PER_RUN_PCT / 100);
previous_cap = run.cap;
}
let steady_cap = previous_cap;
assert!(steady_cap >= 10 * gib + MIN_STEADY_HEADROOM_BYTES);
let spike = synthetic_voyage(
&mut metrics,
SyntheticTarget {
unrecognized: 256 * gib,
..Default::default()
},
);
assert_eq!(spike.cap, steady_cap);
for _ in 0..GC_METRICS_WINDOW {
let run = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 10 * gib,
..Default::default()
},
);
assert!(run.cap <= steady_cap);
}
}
#[test]
fn healthy_growth_remains_bounded_per_run() {
let gib = 1024 * 1024 * 1024;
let mut metrics = GcMetrics {
last_suggested_cap: Some(12 * gib),
recent_auto_cap_runs: vec![
AutoCapRun {
cap: 12 * gib,
initial_size: 10 * gib,
final_size: 10 * gib,
retained_artifact_bytes: 10 * gib,
..Default::default()
},
AutoCapRun {
cap: 12 * gib,
initial_size: 12 * gib,
final_size: 12 * gib,
retained_artifact_bytes: 12 * gib,
..Default::default()
},
],
..Default::default()
};
let run = synthetic_voyage(
&mut metrics,
SyntheticTarget {
protected_artifacts: 13 * gib,
..Default::default()
},
);
assert_eq!(
run.cap,
12 * gib + 12 * gib * MAX_GROWTH_FACTOR_PER_RUN_PCT / 100
);
assert_eq!(
metrics.last_cap_trace.as_ref().unwrap().sample_source,
CAP_TRACE_SAMPLE_SOURCE_HEALTHY
);
}