use super::cache::{
GcActionStoreReport, GcIncrementalReport, GcReport, GcTargetReport, print_json,
};
use crate::config::{Config, RetentionSettings};
use crate::{store, target};
use bytesize::ByteSize;
use eyre::Result;
use std::path::Path;
#[derive(usage::Args)]
pub(super) struct GcArgs {
#[usage(long, value_name = "SIZE")]
pub(super) max_size: Option<ByteSize>,
#[usage(long)]
pub(super) json: bool,
#[usage(long)]
pub(super) dry_run: bool,
}
pub(super) fn run(
config: &Config,
max_bytes: u64,
dry_run: bool,
json: bool,
retention: &RetentionSettings,
) -> Result<()> {
let store = config.store_dir();
let incremental = match crate::incremental::collect(
&config.cache_dir.join("incremental"),
incremental_budget(retention, max_bytes),
retention.incremental_max_age,
dry_run,
) {
Ok(outcome) => outcome,
Err(error) => {
log::warn!("learned incremental state was not collected: {error}");
let stats = crate::incremental::stats(&config.cache_dir.join("incremental"))
.unwrap_or_default();
crate::incremental::PruneOutcome {
remaining_directories: stats.directories,
remaining_bytes: stats.bytes,
untracked_directories: stats.untracked_directories,
..crate::incremental::PruneOutcome::default()
}
}
};
let target_budget = target_budget(retention, max_bytes, incremental.remaining_bytes);
let pruned = target::collect(
&config.target.root,
target_budget,
retention.target_max_age,
dry_run,
);
let projected_target_bytes = match &pruned {
Ok(outcome) => outcome.remaining_bytes,
Err(_) if retention.max_total_bytes.is_some() => target::stats(&config.target.root)
.map(|stats| stats.bytes)
.unwrap_or_default(),
Err(_) => 0,
};
let store_budget = store_budget(
retention,
max_bytes,
projected_target_bytes.saturating_add(incremental.remaining_bytes),
);
if !dry_run {
crate::scheduler::prune_flights(&config.cache_dir);
}
let outcome = if dry_run {
store::gc_dry_run(&store, store_budget)
} else {
store::gc(&store, store_budget)
};
let outcome = match outcome {
Ok(outcome) => outcome,
Err(error) => {
let mut freed_bytes = incremental.removed_bytes;
match pruned {
Ok(pruned) => {
freed_bytes = freed_bytes.saturating_add(pruned.removed_bytes);
if !json && pruned.removed_views > 0 {
println!("{}", target_removals(&pruned, dry_run));
}
}
Err(prune_error) => {
log::warn!("target directories were not collected: {prune_error}");
}
}
record_collection(&store, 0, freed_bytes, dry_run);
if !json {
print_incremental_removals(&incremental, dry_run);
}
return Err(error);
}
};
record_collection(
&store,
outcome.removed_bytes,
pruned.as_ref().map_or(0, |pruned| pruned.removed_bytes) + incremental.removed_bytes,
dry_run,
);
if json {
let pruned = pruned?;
print_json(&GcReport {
version: 1,
byte_accounting: "logical",
max_bytes,
max_total_bytes: retention.max_total_bytes,
target_max_bytes: retention.target_max_bytes,
incremental_max_bytes: retention.incremental_max_bytes,
dry_run,
action_store: GcActionStoreReport {
removed_objects: outcome.removed_objects,
removed_action_results: outcome.removed_action_results,
removed_checkout_records: outcome.removed_checkout_records,
removed_session_streams: outcome.removed_session_streams,
removed_bytes: outcome.removed_bytes,
remaining_bytes: outcome.remaining_bytes,
},
targets: GcTargetReport {
removed_directories: pruned.removed_views,
removed_bytes: pruned.removed_bytes,
remaining_directories: pruned.remaining_views,
remaining_bytes: pruned.remaining_bytes,
},
incremental: GcIncrementalReport {
removed_directories: incremental.removed_directories,
removed_bytes: incremental.removed_bytes,
remaining_directories: incremental.remaining_directories,
remaining_bytes: incremental.remaining_bytes,
skipped_active_directories: incremental.skipped_active_directories,
untracked_directories: incremental.untracked_directories,
},
})?;
} else {
print_gc_store_outcome(&outcome, dry_run);
print_incremental_removals(&incremental, dry_run);
let pruned = pruned?;
if pruned.removed_views > 0 {
println!("{}", target_removals(&pruned, dry_run));
}
}
Ok(())
}
fn print_incremental_removals(outcome: &crate::incremental::PruneOutcome, dry_run: bool) {
if outcome.removed_directories == 0
&& outcome.skipped_active_directories == 0
&& outcome.untracked_directories == 0
{
return;
}
let verb = if dry_run { "would remove" } else { "removed" };
println!(
"{verb} {} learned incremental directories ({} logical); {} logical remain",
outcome.removed_directories,
ByteSize::b(outcome.removed_bytes).display().iec(),
ByteSize::b(outcome.remaining_bytes).display().iec(),
);
if outcome.skipped_active_directories > 0 {
println!(
"kept {} learned incremental directories used by active builds",
outcome.skipped_active_directories
);
}
if outcome.untracked_directories > 0 {
println!(
"kept {} learned incremental directories with unreadable checkout records",
outcome.untracked_directories
);
}
}
pub(super) fn record_collection(store: &Path, store_bytes: u64, target_bytes: u64, dry_run: bool) {
if dry_run || (store_bytes == 0 && target_bytes == 0) {
return;
}
crate::savings::record_quietly(
store,
&crate::savings::Delta {
freed_store_bytes: store_bytes,
freed_target_bytes: target_bytes,
..crate::savings::Delta::default()
},
);
}
pub(super) fn print_gc_store_outcome(outcome: &store::GcOutcome, dry_run: bool) {
let prefix = if dry_run { "would have " } else { "" };
println!("{prefix}{}", evictions(outcome));
if outcome.removed_checkout_records > 0 {
println!(
"{prefix}dropped {} stale checkout records",
outcome.removed_checkout_records
);
}
if outcome.removed_session_streams > 0 {
println!(
"{prefix}dropped {} session event streams",
outcome.removed_session_streams
);
}
}
pub(super) fn target_removals(outcome: &target::CollectionOutcome, dry_run: bool) -> String {
let verb = if dry_run { "would remove" } else { "removed" };
format!(
"{verb} {} target directories ({} logical, {} abandoned and {} live); {} logical remain",
outcome.removed_views,
ByteSize::b(outcome.removed_bytes).display().iec(),
outcome.removed_stale_views,
outcome.removed_live_views,
ByteSize::b(outcome.remaining_bytes).display().iec(),
)
}
pub(super) fn evictions(outcome: &store::GcOutcome) -> String {
format!(
"evicted {} objects and {} action results ({} logical); {} logical remain",
outcome.removed_objects,
outcome.removed_action_results,
ByteSize::b(outcome.removed_bytes).display().iec(),
ByteSize::b(outcome.remaining_bytes).display().iec(),
)
}
pub(super) fn sweep_store(config: &Config, retention: &RetentionSettings) -> crate::savings::Delta {
let mut delta = crate::savings::Delta::default();
if !config.gc.auto {
return delta;
}
match store::claim_sweep(&config.store_dir(), config.gc.interval) {
Ok(false) => {}
Ok(true) => {
let pruned = prune_targets(config, retention, config.gc.max_bytes);
delta.freed_target_bytes = pruned.freed_bytes;
let non_store_bytes = pruned.remaining_bytes.unwrap_or_else(|| {
let target_bytes =
target::stats(&config.target.root).map_or(0, |stats| stats.bytes);
let incremental_bytes =
crate::incremental::stats(&config.cache_dir.join("incremental"))
.map_or(0, |stats| stats.bytes);
target_bytes.saturating_add(incremental_bytes)
});
let store_budget = store_budget(retention, config.gc.max_bytes, non_store_bytes);
crate::scheduler::prune_flights(&config.cache_dir);
let outcome = match store::gc(&config.store_dir(), store_budget) {
Ok(outcome) => outcome,
Err(error) => {
log::warn!("the store was not swept: {error}");
return delta;
}
};
delta.freed_store_bytes = outcome.removed_bytes;
if outcome.removed_bytes > 0 {
crate::session::note(&format!("mbx[gc]: {}", evictions(&outcome)));
}
}
Err(error) => {
log::warn!("the store was not swept: {error}");
delta.freed_target_bytes =
prune_targets(config, retention, config.gc.max_bytes).freed_bytes;
}
}
delta
}
pub(super) struct PruneReport {
remaining_bytes: Option<u64>,
freed_bytes: u64,
}
pub(super) fn prune_targets(
config: &Config,
retention: &RetentionSettings,
store_reserve: u64,
) -> PruneReport {
let incremental = crate::incremental::collect(
&config.cache_dir.join("incremental"),
incremental_budget(retention, store_reserve),
retention.incremental_max_age,
false,
);
let (incremental_bytes, incremental_remaining) = match incremental {
Ok(outcome) => (outcome.removed_bytes, outcome.remaining_bytes),
Err(error) => {
log::warn!("learned incremental state was not collected: {error}");
let remaining = crate::incremental::stats(&config.cache_dir.join("incremental"))
.map_or(0, |stats| stats.bytes);
(0, remaining)
}
};
let target_budget = target_budget(retention, store_reserve, incremental_remaining);
match target::collect(
&config.target.root,
target_budget,
retention.target_max_age,
false,
) {
Ok(pruned) => {
if pruned.removed_views > 0 {
crate::session::note(&format!("mbx[gc]: {}", target_removals(&pruned, false)));
}
PruneReport {
remaining_bytes: Some(pruned.remaining_bytes.saturating_add(incremental_remaining)),
freed_bytes: pruned.removed_bytes.saturating_add(incremental_bytes),
}
}
Err(error) => {
log::warn!("target directories were not collected: {error}");
PruneReport {
remaining_bytes: None,
freed_bytes: incremental_bytes,
}
}
}
}
pub(super) fn target_budget(
retention: &RetentionSettings,
store_reserve: u64,
incremental_reserve: u64,
) -> Option<u64> {
retention
.max_total_bytes
.map_or(retention.target_max_bytes, |total| {
let combined = total.saturating_sub(store_reserve.saturating_add(incremental_reserve));
Some(
retention
.target_max_bytes
.map_or(combined, |target| target.min(combined)),
)
})
}
pub(super) fn incremental_budget(retention: &RetentionSettings, store_reserve: u64) -> Option<u64> {
retention
.max_total_bytes
.map_or(retention.incremental_max_bytes, |total| {
let combined = total.saturating_sub(store_reserve);
Some(
retention
.incremental_max_bytes
.map_or(combined, |incremental| incremental.min(combined)),
)
})
}
pub(super) fn store_budget(
retention: &RetentionSettings,
max_bytes: u64,
non_store_bytes: u64,
) -> u64 {
retention.max_total_bytes.map_or(max_bytes, |total| {
max_bytes.min(total.saturating_sub(non_store_bytes))
})
}