use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::graph::NodeKind;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum IndexProgress {
Started {
total_files: usize,
},
FileProcessing {
path: PathBuf,
current: usize,
total: usize,
},
FileCompleted {
path: PathBuf,
symbols: usize,
},
IngestProgress {
files_processed: usize,
total_files: usize,
total_symbols: usize,
counts: NodeIngestCounts,
elapsed: Duration,
eta: Option<Duration>,
},
IngestFileStarted {
path: PathBuf,
current: usize,
total: usize,
},
IngestFileCompleted {
path: PathBuf,
symbols: usize,
duration: Duration,
},
StageStarted {
stage_name: &'static str,
},
StageCompleted {
stage_name: &'static str,
stage_duration: Duration,
},
GraphPhaseStarted {
phase_number: u8,
phase_name: &'static str,
total_items: usize,
},
GraphPhaseProgress {
phase_number: u8,
items_processed: usize,
total_items: usize,
},
GraphPhaseCompleted {
phase_number: u8,
phase_name: &'static str,
phase_duration: Duration,
},
SavingStarted {
component_name: &'static str,
},
SavingCompleted {
component_name: &'static str,
save_duration: Duration,
},
Completed {
total_symbols: usize,
duration: Duration,
},
}
pub trait ProgressReporter: Send + Sync {
fn report(&self, event: IndexProgress);
}
pub struct ProgressStage {
reporter: SharedReporter,
stage_name: &'static str,
start: Instant,
}
impl ProgressStage {
#[must_use]
pub fn start(reporter: &SharedReporter, stage_name: &'static str) -> Self {
reporter.report(IndexProgress::StageStarted { stage_name });
Self {
reporter: Arc::clone(reporter),
stage_name,
start: Instant::now(),
}
}
pub fn finish(self) {
self.reporter.report(IndexProgress::StageCompleted {
stage_name: self.stage_name,
stage_duration: self.start.elapsed(),
});
}
}
#[derive(Debug, Clone, Default)]
pub struct NodeIngestCounts {
pub functions: usize,
pub classes: usize,
pub methods: usize,
pub structs: usize,
pub enums: usize,
pub interfaces: usize,
pub variables: usize,
pub constants: usize,
pub types: usize,
pub modules: usize,
pub other: usize,
}
impl NodeIngestCounts {
pub fn add_node_kind(&mut self, kind: &NodeKind) {
match kind {
NodeKind::Function { .. } => self.functions += 1,
NodeKind::Class { .. } => self.classes += 1,
NodeKind::Module { .. } => self.modules += 1,
NodeKind::Variable { .. } => self.variables += 1,
}
}
pub fn add_node_kinds(&mut self, kinds: &[NodeKind]) {
for kind in kinds {
self.add_node_kind(kind);
}
}
#[must_use]
pub fn total(&self) -> usize {
self.functions
+ self.classes
+ self.methods
+ self.structs
+ self.enums
+ self.interfaces
+ self.variables
+ self.constants
+ self.types
+ self.modules
+ self.other
}
}
pub struct IngestProgressTracker {
reporter: SharedReporter,
total_files: usize,
processed_files: usize,
counts: NodeIngestCounts,
start: Instant,
last_emit: Instant,
}
impl IngestProgressTracker {
#[must_use]
pub fn new(reporter: &SharedReporter, total_files: usize) -> Self {
let now = Instant::now();
Self {
reporter: Arc::clone(reporter),
total_files,
processed_files: 0,
counts: NodeIngestCounts::default(),
start: now,
last_emit: now,
}
}
pub fn record_node_kinds(&mut self, kinds: &[NodeKind]) {
self.processed_files = self.processed_files.saturating_add(1);
self.counts.add_node_kinds(kinds);
self.maybe_emit(false);
}
pub fn finish(&mut self) {
self.maybe_emit(true);
}
fn maybe_emit(&mut self, force: bool) {
let now = Instant::now();
let elapsed = now.duration_since(self.start);
if !force && now.duration_since(self.last_emit) < Duration::from_millis(800) {
return;
}
self.last_emit = now;
let eta = self.estimate_eta(elapsed);
self.reporter.report(IndexProgress::IngestProgress {
files_processed: self.processed_files,
total_files: self.total_files,
total_symbols: self.counts.total(),
counts: self.counts.clone(),
elapsed,
eta,
});
}
fn estimate_eta(&self, elapsed: Duration) -> Option<Duration> {
if self.processed_files == 0 || self.total_files == 0 {
return None;
}
let elapsed_nanos = elapsed.as_nanos();
if elapsed_nanos == 0 {
return None;
}
let processed_files = u128::from(self.processed_files as u64);
let remaining_files =
u128::from(self.total_files.saturating_sub(self.processed_files) as u64);
if processed_files == 0 || remaining_files == 0 {
return Some(Duration::from_secs(0));
}
let nanos_per_file = elapsed_nanos / processed_files;
let remaining_nanos = nanos_per_file.saturating_mul(remaining_files);
let remaining_nanos_u64 = u64::try_from(remaining_nanos).ok()?;
Some(Duration::from_nanos(remaining_nanos_u64))
}
}
#[derive(Debug, Clone, Copy)]
pub struct NoOpReporter;
impl ProgressReporter for NoOpReporter {
fn report(&self, _event: IndexProgress) {
}
}
pub type SharedReporter = Arc<dyn ProgressReporter>;
#[must_use]
pub fn no_op_reporter() -> SharedReporter {
Arc::new(NoOpReporter)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
struct TestReporter {
events: Mutex<Vec<IndexProgress>>,
}
impl TestReporter {
fn new() -> Self {
Self {
events: Mutex::new(Vec::new()),
}
}
fn events(&self) -> Vec<IndexProgress> {
self.events.lock().unwrap().clone()
}
}
impl ProgressReporter for TestReporter {
fn report(&self, event: IndexProgress) {
self.events.lock().unwrap().push(event);
}
}
#[test]
fn test_progress_event_sequence() {
let reporter = TestReporter::new();
reporter.report(IndexProgress::Started { total_files: 2 });
reporter.report(IndexProgress::FileProcessing {
path: PathBuf::from("file1.rs"),
current: 1,
total: 2,
});
reporter.report(IndexProgress::FileCompleted {
path: PathBuf::from("file1.rs"),
symbols: 10,
});
reporter.report(IndexProgress::FileProcessing {
path: PathBuf::from("file2.rs"),
current: 2,
total: 2,
});
reporter.report(IndexProgress::FileCompleted {
path: PathBuf::from("file2.rs"),
symbols: 15,
});
reporter.report(IndexProgress::Completed {
total_symbols: 25,
duration: Duration::from_secs(1),
});
let events = reporter.events();
assert_eq!(events.len(), 6);
matches!(events[0], IndexProgress::Started { .. });
matches!(events[1], IndexProgress::FileProcessing { .. });
matches!(events[2], IndexProgress::FileCompleted { .. });
matches!(events[3], IndexProgress::FileProcessing { .. });
matches!(events[4], IndexProgress::FileCompleted { .. });
matches!(events[5], IndexProgress::Completed { .. });
}
#[test]
fn test_no_op_reporter() {
let reporter = no_op_reporter();
reporter.report(IndexProgress::Started { total_files: 5 });
reporter.report(IndexProgress::Completed {
total_symbols: 100,
duration: Duration::from_millis(500),
});
}
#[test]
fn test_reporter_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<NoOpReporter>();
assert_send_sync::<TestReporter>();
}
}