diff --git a/src/cli/lib.rs b/src/cli/lib.rs index 7f75d735..1091d59a 100644 --- a/src/cli/lib.rs +++ b/src/cli/lib.rs @@ -1647,6 +1647,7 @@ async fn do_analysis( find_unused_expressions, find_unused_definitions, block_until_next_analysis: false, + send_progress_report: true, }); // Poll for results, showing progress bar if analysis is in progress @@ -1698,15 +1699,25 @@ async fn do_analysis( return; } else { // Update progress bar - pb.set_length(result.total_files.max(1) as u64); - pb.set_position(result.files_analyzed as u64); - pb.set_message(format!( - "{} ({}/{} files)", - result.phase, result.files_analyzed, result.total_files - )); + let is_analyzing = result.files_analyzed > 0; + if is_analyzing { + pb.set_length(result.total_files_to_analyze.max(1) as u64); + pb.set_position(result.files_analyzed as u64); + pb.set_message(format!( + "{} ({}/{} files)", + result.phase, result.files_analyzed, result.total_files_to_analyze + )); + } else { + pb.set_length(result.total_files_to_scan.max(1) as u64); + pb.set_position(result.files_scanned as u64); + pb.set_message(format!( + "{} ({}/{} files)", + result.phase, result.files_scanned, result.total_files_to_scan + )); + } // Wait a bit before polling again (100ms for responsive UI) - std::thread::sleep(Duration::from_millis(100)); + tokio::time::sleep(Duration::from_millis(100)).await; } } Ok(Message::Error(err)) => { diff --git a/src/language_server/server_backend.rs b/src/language_server/server_backend.rs index 76fd383e..0378d0c2 100644 --- a/src/language_server/server_backend.rs +++ b/src/language_server/server_backend.rs @@ -50,10 +50,7 @@ impl ServerBasedBackend { client .log_message( MessageType::INFO, - format!( - "Server analysis in progress: {} ({}%)", - response.phase, response.progress_percent - ), + format!("Server analysis in progress: {}", response.phase), ) .await; // Don't update diagnostics while analysis is in progress diff --git a/src/language_server/server_client.rs b/src/language_server/server_client.rs index 2d0b851f..af12fc29 100644 --- a/src/language_server/server_client.rs +++ b/src/language_server/server_client.rs @@ -243,6 +243,7 @@ impl ServerConnection { find_unused_expressions, find_unused_definitions, block_until_next_analysis, + send_progress_report: false, }); match socket.request(&request).await { diff --git a/src/orchestrator/lib.rs b/src/orchestrator/lib.rs index dd00ef02..c5ea4b7e 100644 --- a/src/orchestrator/lib.rs +++ b/src/orchestrator/lib.rs @@ -24,6 +24,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use scanner::{ScanFilesResult, scan_files}; use std::fs; use std::io::{self, Write}; +use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use unused_symbols::find_unused_definitions; @@ -72,7 +73,12 @@ impl Default for SuccessfulScanData { } } -use std::sync::atomic::AtomicU32; +pub struct AnalysisProgress { + pub files_scanned: Arc, + pub total_files_to_scan: Arc, + pub files_analyzed: Arc, + pub total_files_to_analyze: Arc, +} pub fn scan_and_analyze( stubs_dirs: Vec, @@ -104,8 +110,6 @@ pub fn scan_and_analyze( language_server_changes, chaos_monkey, None, - None, - None, ) } @@ -123,15 +127,19 @@ pub fn scan_and_analyze_with_progress( previous_analysis_result: Option, language_server_changes: Option>, chaos_monkey: F, - files_scanned: Option>, - total_files_to_scan: Option>, - files_analyzed: Option>, + analysis_progress: Option, ) -> io::Result<(AnalysisResult, SuccessfulScanData)> { let mut all_scanned_dirs = stubs_dirs.clone(); all_scanned_dirs.push(config.root_dir.clone()); let file_discovery_and_scanning_now = Instant::now(); + let files_scanned = analysis_progress.as_ref().map(|p| p.files_scanned.clone()); + let total_files_to_scan = analysis_progress + .as_ref() + .map(|p| p.total_files_to_scan.clone()); + let files_analyzed = analysis_progress.as_ref().map(|p| p.files_analyzed.clone()); + logger.log_sync("Scanning files"); let ScanFilesResult { @@ -267,6 +275,12 @@ pub fn scan_and_analyze_with_progress( ); logger.log_sync(&format!("Analyzing {} files", files_to_analyze.len())); + if let Some(total_files_to_analyze) = analysis_progress + .as_ref() + .map(|p| p.total_files_to_analyze.clone()) + { + total_files_to_analyze.store(files_to_analyze.len() as u32, Ordering::Relaxed); + } let mut pure_file_analysis_time = Duration::default(); @@ -484,17 +498,6 @@ fn emit_duplicate_definition_issues( issues } -/// Progress information passed to the progress callback. -#[derive(Debug, Clone)] -pub struct AnalysisProgress { - /// Current phase name. - pub phase: String, - /// Number of files analyzed so far (only meaningful during "Analyzing" phase). - pub files_analyzed: u32, - /// Total number of files to analyze. - pub total_files: u32, -} - fn get_analysis_ready( config: &Arc, codebase: CodebaseInfo, diff --git a/src/protocol/serialize.rs b/src/protocol/serialize.rs index 8aba8c95..f092935d 100644 --- a/src/protocol/serialize.rs +++ b/src/protocol/serialize.rs @@ -672,6 +672,7 @@ impl Serialize for GetIssuesRequest { write_bool(buf, self.find_unused_expressions); write_bool(buf, self.find_unused_definitions); write_bool(buf, self.block_until_next_analysis); + write_bool(buf, self.send_progress_report); } } @@ -681,12 +682,14 @@ impl Deserialize for GetIssuesRequest { let (find_unused_expressions, rest) = read_bool(rest)?; let (find_unused_definitions, rest) = read_bool(rest)?; let (block_until_next_analysis, rest) = read_bool(rest)?; + let (send_progress_report, rest) = read_bool(rest)?; Ok(( Self { filter, find_unused_expressions, find_unused_definitions, block_until_next_analysis, + send_progress_report, }, rest, )) @@ -702,10 +705,11 @@ impl Serialize for GetIssuesResponse { for issue in &self.issues { issue.serialize(buf); } + write_u32(buf, self.files_scanned); + write_u32(buf, self.total_files_to_scan); write_u32(buf, self.files_analyzed); - write_u32(buf, self.total_files); + write_u32(buf, self.total_files_to_analyze); write_string(buf, &self.phase); - write_u8(buf, self.progress_percent); } } @@ -719,18 +723,20 @@ impl Deserialize for GetIssuesResponse { issues.push(issue); rest = r; } + let (files_scanned, rest) = read_u32(rest)?; + let (total_files_to_scan, rest) = read_u32(rest)?; let (files_analyzed, rest) = read_u32(rest)?; - let (total_files, rest) = read_u32(rest)?; + let (total_files_to_analyze, rest) = read_u32(rest)?; let (phase, rest) = read_string(rest)?; - let (progress_percent, rest) = read_u8(rest)?; Ok(( Self { analysis_complete, issues, + files_scanned, + total_files_to_scan, files_analyzed, - total_files, + total_files_to_analyze, phase, - progress_percent, }, rest, )) diff --git a/src/protocol/types.rs b/src/protocol/types.rs index 8b8194a3..37970909 100644 --- a/src/protocol/types.rs +++ b/src/protocol/types.rs @@ -236,6 +236,8 @@ pub struct GetIssuesRequest { pub find_unused_definitions: bool, /// Whether to wait until the next analysis run. pub block_until_next_analysis: bool, + /// Whether to send progress reports before the analysis is complete. + pub send_progress_report: bool, } /// Response with current issues. @@ -245,14 +247,16 @@ pub struct GetIssuesResponse { pub analysis_complete: bool, /// Issues found during analysis (may be partial if analysis_complete is false). pub issues: Vec, + /// Number of files scanned so far. + pub files_scanned: u32, + /// Total number of files to scan (0 if unknown). + pub total_files_to_scan: u32, /// Number of files analyzed so far. pub files_analyzed: u32, /// Total number of files to analyze (0 if unknown). - pub total_files: u32, + pub total_files_to_analyze: u32, /// Current analysis phase description. pub phase: String, - /// Progress percentage (0-100). - pub progress_percent: u8, } /// Request for server status. diff --git a/src/server/handler.rs b/src/server/handler.rs index f9be4e55..7cb909ac 100644 --- a/src/server/handler.rs +++ b/src/server/handler.rs @@ -270,14 +270,33 @@ impl RequestHandler { req: hakana_protocol::GetIssuesRequest, ) -> Message { if !req.block_until_next_analysis { - let state = self.state.lock().unwrap(); - let analysis_complete = !state.is_analysis_in_progress(); + let analysis_result = { + let state = self.state.lock().unwrap(); + state + .analysis_data + .as_ref() + .filter(|_| !state.is_analysis_in_progress()) + .map(|r| r.clone()) + }; - if analysis_complete && let Some(analysis_result) = &state.analysis_data { - return self.create_get_issues_response(req, analysis_result); + if let Some(analysis_result) = analysis_result { + return self.create_get_issues_response(req, &analysis_result); } } + if req.send_progress_report { + let state = self.state.lock().unwrap(); + return Message::GetIssuesResult(GetIssuesResponse { + analysis_complete: false, + issues: vec![], + files_scanned: state.files_scanned(), + total_files_to_scan: state.total_files_to_scan(), + files_analyzed: state.files_analyzed(), + total_files_to_analyze: state.total_files_to_analyze(), + phase: state.phase().to_string(), + }); + } + if let Ok(result) = analysis_rx.recv().await && let Ok(result) = result.as_ref() { @@ -286,10 +305,11 @@ impl RequestHandler { Message::GetIssuesResult(GetIssuesResponse { analysis_complete: false, issues: vec![], + files_scanned: 0, + total_files_to_scan: 0, files_analyzed: 0, - total_files: 0, + total_files_to_analyze: 0, phase: "Complete".to_string(), - progress_percent: 100, }) } @@ -318,13 +338,16 @@ impl RequestHandler { self.logger .log_sync(&format!("Returning {} issues", issues.len())); + let state = self.state.lock().unwrap(); + return Message::GetIssuesResult(GetIssuesResponse { analysis_complete: true, issues, - files_analyzed: 0, - total_files: 0, + files_scanned: state.files_scanned(), + total_files_to_scan: state.total_files_to_scan(), + files_analyzed: state.files_analyzed(), + total_files_to_analyze: state.total_files_to_analyze(), phase: "Complete".to_string(), - progress_percent: 100, }); } diff --git a/src/server/lib.rs b/src/server/lib.rs index 5658ef64..a6a367b5 100644 --- a/src/server/lib.rs +++ b/src/server/lib.rs @@ -9,8 +9,8 @@ use hakana_analyzer::config::Config; use hakana_analyzer::custom_hook::CustomHook; use hakana_code_info::analysis_result::AnalysisResult; use hakana_logger::Logger; -use hakana_orchestrator::SuccessfulScanData; use hakana_orchestrator::file::FileStatus; +use hakana_orchestrator::{AnalysisProgress, SuccessfulScanData}; use hakana_protocol::{ ClientConnection, ErrorCode, ErrorResponse, Message, ServerSocket, SocketPath, }; @@ -18,6 +18,7 @@ use hakana_str::Interner; use rustc_hash::{FxHashMap, FxHashSet}; use std::io; use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicU32; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::Instant; @@ -212,11 +213,26 @@ impl Server { let logger = self.logger.clone(); let previous_analysis_data = state.analysis_data.take(); + let files_scanned = state.files_scanned.clone(); + let total_files_to_scan = state.total_files_to_scan.clone(); + + let files_analyzed = state.files_analyzed.clone(); + let total_files_to_analyze = state.total_files_to_analyze.clone(); + let tx = self.analysis_tx.clone(); tokio::task::spawn_blocking(move || { - let result = - run_analysis(&config, &logger, previous_analysis_data, changes).map(&Arc::new); + let result = run_analysis( + &config, + &logger, + previous_analysis_data, + changes, + files_scanned, + total_files_to_scan, + files_analyzed, + total_files_to_analyze, + ) + .map(&Arc::new); let _ = tx.send(result); }); } @@ -360,6 +376,10 @@ fn run_analysis( logger: &Arc, previous_analysis_data: Option>, changes: Option>, + files_scanned: Arc, + total_files_to_scan: Arc, + files_analyzed: Arc, + total_files_to_analyze: Arc, ) -> Result<(AnalysisResult, SuccessfulScanData), String> { let all_custom_issues: FxHashSet = config .plugins @@ -398,7 +418,19 @@ fn run_analysis( .map(|d| (Some(d.1.clone()), Some(d.0.clone()))) .unwrap_or((None, None)); - hakana_orchestrator::scan_and_analyze( + files_scanned.store(0, std::sync::atomic::Ordering::Relaxed); + total_files_to_scan.store(0, std::sync::atomic::Ordering::Relaxed); + files_analyzed.store(0, std::sync::atomic::Ordering::Relaxed); + total_files_to_analyze.store(0, std::sync::atomic::Ordering::Relaxed); + + let progress = AnalysisProgress { + files_scanned, + total_files_to_scan, + files_analyzed, + total_files_to_analyze, + }; + + hakana_orchestrator::scan_and_analyze_with_progress( Vec::new(), None, None, @@ -412,6 +444,7 @@ fn run_analysis( previous_analysis_result, changes, || {}, + Some(progress), ) .map_err(|e| e.to_string()) } diff --git a/src/server/state.rs b/src/server/state.rs index 0498a9b5..8695224a 100644 --- a/src/server/state.rs +++ b/src/server/state.rs @@ -1,6 +1,7 @@ //! Server state management. use std::sync::Arc; +use std::sync::atomic::AtomicU32; use hakana_code_info::analysis_result::AnalysisResult; use hakana_orchestrator::SuccessfulScanData; @@ -18,10 +19,14 @@ pub struct ServerState { pending_requests: u32, /// Current analysis phase description. phase: String, + /// Files scanned so far (during analysis). + pub files_scanned: Arc, + /// Total files to scan. + pub total_files_to_scan: Arc, /// Files analyzed so far (during analysis). - files_analyzed: u32, + pub files_analyzed: Arc, /// Total files to analyze. - total_files: u32, + pub total_files_to_analyze: Arc, /// Pending file changes. pub pending_changes: FxHashMap, } @@ -34,8 +39,10 @@ impl ServerState { analysis_in_progress: false, pending_requests: 0, phase: "Initializing".to_string(), - files_analyzed: 0, - total_files: 0, + files_scanned: Arc::new(0.into()), + total_files_to_scan: Arc::new(0.into()), + files_analyzed: Arc::new(0.into()), + total_files_to_analyze: Arc::new(0.into()), pending_changes: FxHashMap::default(), } } @@ -75,29 +82,24 @@ impl ServerState { self.phase = phase; } - /// Get files analyzed count. - pub fn files_analyzed(&self) -> u32 { - self.files_analyzed + pub fn files_scanned(&self) -> u32 { + self.files_scanned + .load(std::sync::atomic::Ordering::Relaxed) } - /// Get total files count. - pub fn total_files(&self) -> u32 { - self.total_files + pub fn total_files_to_scan(&self) -> u32 { + self.total_files_to_scan + .load(std::sync::atomic::Ordering::Relaxed) } - /// Get progress percentage. - pub fn progress_percent(&self) -> u8 { - if self.total_files == 0 { - 0 - } else { - ((self.files_analyzed as f64 / self.total_files as f64) * 100.0) as u8 - } + pub fn files_analyzed(&self) -> u32 { + self.files_analyzed + .load(std::sync::atomic::Ordering::Relaxed) } - /// Set progress counters during analysis. - pub fn set_progress(&mut self, files_analyzed: u32, total_files: u32) { - self.files_analyzed = files_analyzed; - self.total_files = total_files; + pub fn total_files_to_analyze(&self) -> u32 { + self.total_files_to_analyze + .load(std::sync::atomic::Ordering::Relaxed) } /// Update scan data and analysis result. diff --git a/src/server/tests/integration_test.rs b/src/server/tests/integration_test.rs index 862e614d..a9d1b789 100644 --- a/src/server/tests/integration_test.rs +++ b/src/server/tests/integration_test.rs @@ -163,6 +163,7 @@ async fn test_server_client_get_issues() { find_unused_expressions: false, find_unused_definitions: false, block_until_next_analysis: false, + send_progress_report: false, }); let response = client