#[cfg(feature = "native")]
use std::time::{Duration, Instant};
#[cfg(feature = "native")]
const PROGRESS_INTERVAL: Duration = Duration::from_secs(30);
pub(crate) struct PhaseProgress {
index: String,
label: &'static str,
detail: String,
total: usize,
report: bool,
#[cfg(feature = "native")]
started: Instant,
#[cfg(feature = "native")]
last_report: Instant,
}
impl PhaseProgress {
pub(crate) fn start(index: &str, label: &'static str, detail: String, total: usize) -> Self {
Self::start_if(true, index, label, detail, total)
}
pub(crate) fn start_if(
report: bool,
index: &str,
label: &'static str,
detail: String,
total: usize,
) -> Self {
if report {
log::info!("[vector_training] {label} started: index={index} {detail}");
}
Self {
index: index.to_owned(),
label,
detail,
total,
report,
#[cfg(feature = "native")]
started: Instant::now(),
#[cfg(feature = "native")]
last_report: Instant::now(),
}
}
#[allow(unused_variables)]
pub(crate) fn advance(&mut self, done: usize) {
if !self.report {
return;
}
#[cfg(feature = "native")]
{
let now = Instant::now();
if now.duration_since(self.last_report) < PROGRESS_INTERVAL || done == 0 {
return;
}
self.last_report = now;
let elapsed = now.duration_since(self.started);
let percent = if self.total == 0 {
0.0
} else {
100.0 * done as f64 / self.total as f64
};
let remaining = if done == 0 || done >= self.total {
Duration::ZERO
} else {
elapsed.mul_f64((self.total - done) as f64 / done as f64)
};
log::info!(
"[vector_training] {} index={} {}/{} ({percent:.1}%) in {:.1}s, ~{:.0}s left: {}",
self.label,
self.index,
done,
self.total,
elapsed.as_secs_f64(),
remaining.as_secs_f64(),
self.detail,
);
}
}
pub(crate) fn shared(&self) -> SharedProgress {
SharedProgress {
index: self.index.clone(),
label: self.label,
total: self.total,
done: std::sync::atomic::AtomicUsize::new(0),
step: self.total.div_ceil(SHARED_PROGRESS_STEPS).max(1),
}
}
pub(crate) fn finish(self) {
if !self.report {
return;
}
#[cfg(feature = "native")]
log::info!(
"[vector_training] {} complete in {:.1}s: index={} {}",
self.label,
self.started.elapsed().as_secs_f64(),
self.index,
self.detail,
);
#[cfg(not(feature = "native"))]
log::info!(
"[vector_training] {} complete: index={} {}",
self.label,
self.index,
self.detail
);
}
}
const SHARED_PROGRESS_STEPS: usize = 20;
pub(crate) struct SharedProgress {
index: String,
label: &'static str,
total: usize,
done: std::sync::atomic::AtomicUsize,
step: usize,
}
impl SharedProgress {
pub(crate) fn complete_one(&self) {
let done = self.done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
if done.is_multiple_of(self.step) && done < self.total {
log::info!(
"[vector_training] {} index={} {done}/{} ({:.0}%)",
self.label,
self.index,
self.total,
100.0 * done as f64 / self.total as f64,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, OnceLock};
static CAPTURED: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
struct CaptureLogger;
impl log::Log for CaptureLogger {
fn enabled(&self, _: &log::Metadata<'_>) -> bool {
true
}
fn log(&self, record: &log::Record<'_>) {
CAPTURED
.get_or_init(Default::default)
.lock()
.unwrap()
.push(record.args().to_string());
}
fn flush(&self) {}
}
#[test]
fn training_progress_lines_carry_the_index_name() {
let _ = log::set_boxed_logger(Box::new(CaptureLogger));
log::set_max_level(log::LevelFilter::Info);
let phase = PhaseProgress::start(
"documents_20260724",
"k-majority seeding",
"163342 centroids".into(),
4,
);
phase.shared().complete_one();
phase.finish();
let silent = PhaseProgress::start_if(
false,
"social_20260724",
"child codebooks",
"quiet".into(),
1,
);
silent.finish();
let lines = CAPTURED
.get_or_init(Default::default)
.lock()
.unwrap()
.clone();
assert!(
lines
.iter()
.all(|line| line.contains("index=documents_20260724")),
"unlabelled training line: {lines:?}"
);
assert!(
lines.iter().any(|line| line
.contains("k-majority seeding started: index=documents_20260724 163342 centroids")),
"start line shape changed: {lines:?}"
);
assert!(
lines
.iter()
.any(|line| line.contains("k-majority seeding complete in")),
"missing completion line: {lines:?}"
);
assert!(
!lines.iter().any(|line| line.contains("social")),
"suppressed phase must stay silent: {lines:?}"
);
}
}