deb,report,ui: report the staged-tree copy while a record forks

Forking an incremental base copies its whole staged tree — bounded,
and without reflinks a matter of minutes — but the copy emitted no
build events: the live view showed the target line with nothing below
it, and an interrupt there printed raw cp errors with no hint of what
was running (the fork happens before the first bootstrap phase owns
the view).

Announce the copy through a new BuildView::byte_progress port. The
fork sizes the source up front (the copy's total), re-measures the
growing destination from a scoped thread while cp runs (cp has no
progress output to tap), and reports the completed copy when it
returns; unmeasurable or empty sources copy unreported. The terminal
view renders it like the other determinate bars, with byte-unit
counters.
This commit is contained in:
2026-10-03 01:28:30 +02:00
parent 83a853d259
commit f6f2b882a8
4 changed files with 227 additions and 16 deletions
+46 -1
View File
@@ -46,6 +46,9 @@ struct Pipeline {
errors: Vec<String>,
last_draw: Instant,
bar_total: u64,
/// Whether the bar's counters are bytes ([`BuildView::byte_progress`])
/// rather than plain counts
bar_bytes: bool,
}
/// State shared between [`DebUi`] and its sinks
@@ -119,6 +122,7 @@ impl DebUi {
errors: Vec::new(),
last_draw: Instant::now(),
bar_total: 0,
bar_bytes: false,
}),
tee: Mutex::new(None),
log_path: Mutex::new(log_path),
@@ -195,6 +199,7 @@ impl DebUi {
st.classifier = classifier;
st.lines.clear();
st.bar_total = 0;
st.bar_bytes = false;
st.last_draw = Instant::now();
}
if self.active() {
@@ -309,6 +314,19 @@ impl crate::report::BuildView for DebUi {
self.shared.top.set_message(label.to_string());
}
fn byte_progress(&self, label: &str, pos: u64, total: u64) {
if !self.active() || total == 0 {
return;
}
apply_byte_progress(
&self.shared.top,
&mut self.shared.state.lock().unwrap(),
pos,
total,
);
self.shared.top.set_message(label.to_string());
}
fn sink(&self) -> Option<Arc<dyn LineSink>> {
Some(Arc::new(Sink {
shared: self.shared.clone(),
@@ -384,7 +402,8 @@ fn apply_progress(top: &ProgressBar, st: &mut Pipeline, pos: u64, total: u64) {
if total == 0 {
return;
}
if st.bar_total != total {
if st.bar_bytes || st.bar_total != total {
st.bar_bytes = false;
st.bar_total = total;
top.set_style(determinate_style());
top.set_length(total);
@@ -392,6 +411,21 @@ fn apply_progress(top: &ProgressBar, st: &mut Pipeline, pos: u64, total: u64) {
top.set_position(pos.min(total));
}
/// Update the determinate bar with byte counters (human-formatted by the
/// template), restyling it when needed
fn apply_byte_progress(top: &ProgressBar, st: &mut Pipeline, pos: u64, total: u64) {
if total == 0 {
return;
}
if !st.bar_bytes || st.bar_total != total {
st.bar_bytes = true;
st.bar_total = total;
top.set_style(byte_style());
top.set_length(total);
}
top.set_position(pos.min(total));
}
/// Push a line into the rolling pane and redraw (throttled)
///
/// When the pane is full, the oldest non-error line is dropped first so
@@ -530,6 +564,15 @@ fn determinate_style() -> ProgressStyle {
.progress_chars("=> ")
}
/// Status bar style with a determinate progress bar over a byte
/// transfer: the counters render as byte units (`{bytes}`/`{total_bytes}`)
fn byte_style() -> ProgressStyle {
ProgressStyle::default_bar()
.template("> {spinner:.blue} {prefix}\n {msg} [{bar:24.cyan/blue}] {bytes}/{total_bytes}")
.expect("valid template")
.progress_chars("=> ")
}
/// Whether stdout is a terminal
fn is_stdout_tty() -> bool {
// SAFETY: isatty only inspects the file descriptor
@@ -722,6 +765,7 @@ mod tests {
errors: Vec::new(),
last_draw: Instant::now(),
bar_total: 0,
bar_bytes: false,
}),
tee: Mutex::new(None),
log_path: Mutex::new(std::env::temp_dir().join("pkh-pane-test.log")),
@@ -764,6 +808,7 @@ mod tests {
errors: Vec::new(),
last_draw: Instant::now(),
bar_total: 0,
bar_bytes: false,
}),
tee: Mutex::new(None),
log_path: Mutex::new(std::env::temp_dir().join("pkh-interrupt-report-test.log")),