prune,ui: show the removal pass, prefix the sizing spinner
CI / build (push) Successful in 6m23s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 11m45s

Confirming a 500 GiB prune went silent under "Removing:" for minutes:
drive an indicatif bar from the plan's sizes through a new
apply_with_progress observer, in the deb live view's layout — the
running count behind the spinner on the first line, the byte bar with
its counters on the second. The removed path is not rendered: it
fights the bar on a narrow terminal, and the failed-removal errors
name the paths that matter. --force plans no sizes and gets the count
line only.

The dry-run size spinner lacked the "> " prompt prefix the other
one-line waits carry; wait_spinner renders it too now.
This commit is contained in:
2026-10-03 01:34:47 +02:00
parent f6f2b882a8
commit 486856da60
4 changed files with 129 additions and 14 deletions
+70 -2
View File
@@ -810,18 +810,36 @@ pub fn plan_in_roots(
PrunePlan { report, actionable }
}
/// Progress observer of [`apply_with_progress`]: the removal of the
/// `idx`-th of `count` artifact (0-based) starts now, and `done` bytes
/// have been freed by the artifacts removed before it. Sizes are the
/// plan's estimates (measured before the removal; `--force` plans no
/// sizes and reports zeros).
pub type RemovalProgress<'a> = &'a dyn Fn(usize, usize, &Path, u64);
/// Execute a plan: remove the planned artifacts (unmounting the
/// filesystems under residual directories and session roots first) and
/// report what was removed and what failed. A dry-run plan removes
/// nothing and returns its digest unchanged.
pub fn apply(plan: PrunePlan) -> PruneReport {
apply_with_progress(plan, None)
}
/// [`apply`], reporting the removal as it progresses (the big session
/// trees take minutes): each artifact is announced just before it goes.
pub fn apply_with_progress(plan: PrunePlan, progress: Option<RemovalProgress<'_>>) -> PruneReport {
if plan.report.dry_run {
return plan.report;
}
let mut report = plan.report;
report.removed = Vec::new();
for (artifact, _) in &plan.actionable {
let count = plan.actionable.len();
let mut done = 0u64;
for (idx, (artifact, bytes)) in plan.actionable.iter().enumerate() {
let path = artifact.path();
if let Some(progress) = progress {
progress(idx, count, path, done);
}
// Residual build directories may hold bind-mounted /proc and overlay
// filesystems that must be unmounted before the directory can be
@@ -838,7 +856,10 @@ pub fn apply(plan: PrunePlan) -> PruneReport {
}
match remove_path(path) {
Ok(()) => report.removed.push(path.to_path_buf()),
Ok(()) => {
report.removed.push(path.to_path_buf());
done += bytes;
}
Err(e) => report.failed.push((path.to_path_buf(), e.to_string())),
}
}
@@ -1468,6 +1489,53 @@ none /tmp/other proc rw 0 0
assert!(!temp_path.join("pkh-1700000001-2").exists());
}
/// The removal progress announces every artifact just before it goes,
/// with the cumulative bytes of the artifacts already removed: the
/// terminal drives its bar from it while the big trees take minutes.
#[test]
fn apply_reports_each_removal_with_cumulative_sizes() {
let temp = tempdir().unwrap();
let temp_path = temp.path();
let big = temp_path.join("pkh-1700000000");
fs::create_dir_all(&big).unwrap();
fs::write(big.join("obj.o"), vec![0u8; 3000]).unwrap();
let small = temp_path.join("pkh-1700000001");
fs::create_dir_all(&small).unwrap();
fs::write(small.join("o"), vec![0u8; 5]).unwrap();
let plan = plan_in_roots(temp_path, None, None, None, None, PruneOptions::default());
assert_eq!(plan.report().total_bytes, 3005);
let events = std::sync::Mutex::new(Vec::new());
let report = apply_with_progress(
plan,
Some(&|idx, count, path, done| {
events
.lock()
.unwrap()
.push((idx, count, path.to_path_buf(), done));
}),
);
assert_eq!(report.removed.len(), 2);
assert!(!big.exists());
assert!(!small.exists());
let events = events.into_inner().unwrap();
assert_eq!(events.len(), 2);
// The first announcement opens the pass; the second names the
// other artifact and carries the first one's size (discovery
// order is the temp dir's read order, not ours).
assert_eq!(events[0].0, 0);
assert_eq!(events[0].1, 2);
assert_eq!(events[0].3, 0);
assert_eq!(events[1].0, 1);
assert_eq!(events[1].1, 2);
assert_ne!(events[1].2, events[0].2);
let expected_done = if events[0].2 == big { 3000 } else { 5 };
assert_eq!(events[1].3, expected_done);
}
#[test]
fn test_prune_empty_report_when_nothing_found() {
let temp = tempdir().unwrap();