From ef2b2700b56432b60b8987562612917425093abf Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai Date: Mon, 1 Jun 2026 12:31:17 +0530 Subject: [PATCH] perf: parallelize package parsing --- src/managers/apt.rs | 150 ++++++++++++++++++++++++++--------------- src/managers/mod.rs | 60 +++++++++++------ src/managers/pacman.rs | 16 +++-- 3 files changed, 143 insertions(+), 83 deletions(-) diff --git a/src/managers/apt.rs b/src/managers/apt.rs index b76bbb2..0ac9448 100644 --- a/src/managers/apt.rs +++ b/src/managers/apt.rs @@ -1,10 +1,62 @@ use crate::managers::{Package, PackageManager}; use ratatui::DefaultTerminal; +use rayon::prelude::*; use std::collections::{HashMap, HashSet}; use std::process::Command; pub struct AptManager; +fn parse_search_line(query: &str, line: &str) -> Option { + let parts: Vec<&str> = line.splitn(2, " - ").collect(); + if parts.is_empty() { + return None; + } + + let name = parts[0].trim().to_string(); + if name.is_empty() { + return None; + } + + let description = parts.get(1).unwrap_or(&"").trim().to_string(); + let score = crate::fuzzy::fuzzy_match(query, &name); + + if score <= 0.01 { + return None; + } + + Some(Package { provider: "apt".to_string(), name, version: String::new(), description, score }) +} + +fn parse_installed_detail_line(line: &str) -> Option { + let parts: Vec<&str> = line.split('\t').collect(); + if parts.len() < 2 { + return None; + } + + Some(Package { + provider: "apt/local".to_string(), + name: parts[0].to_string(), + version: parts[1].to_string(), + description: parts.get(2).unwrap_or(&"").to_string(), + score: 1.0, + }) +} + +fn parse_update_line(line: &str) -> Option { + let parts: Vec<&str> = line.split('/').collect(); + if parts.is_empty() || parts[0].is_empty() { + return None; + } + + Some(Package { + provider: "apt/update".to_string(), + name: parts[0].to_string(), + version: "Update available".to_string(), + description: String::new(), + score: 1.0, + }) +} + impl PackageManager for AptManager { fn name(&self) -> &str { "APT (Debian/Ubuntu)" @@ -18,27 +70,8 @@ impl PackageManager for AptManager { if let Some(output) = output { let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .lines() - .filter_map(|line| { - let parts: Vec<&str> = line.splitn(2, " - ").collect(); - if !parts.is_empty() { - let name = parts[0].trim().to_string(); - let desc = parts.get(1).unwrap_or(&"").trim().to_string(); - let score = crate::fuzzy::fuzzy_match(query, &name); - Some(Package { - provider: "apt".to_string(), - name, - version: "".to_string(), - description: desc, - score, - }) - } else { - None - } - }) - .filter(|p| p.score > 0.01) - .collect() + let lines: Vec<&str> = stdout.lines().collect(); + lines.par_iter().filter_map(|line| parse_search_line(query, line)).collect() } else { Vec::new() } @@ -63,23 +96,8 @@ impl PackageManager for AptManager { if let Some(output) = output { let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .lines() - .filter_map(|line| { - let parts: Vec<&str> = line.split('\t').collect(); - if parts.len() >= 2 { - Some(Package { - provider: "apt/local".to_string(), - name: parts[0].to_string(), - version: parts[1].to_string(), - description: parts.get(2).unwrap_or(&"").to_string(), - score: 1.0, - }) - } else { - None - } - }) - .collect() + let lines: Vec<&str> = stdout.lines().collect(); + lines.par_iter().filter_map(|line| parse_installed_detail_line(line)).collect() } else { Vec::new() } @@ -90,24 +108,8 @@ impl PackageManager for AptManager { if let Some(output) = output { let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .lines() - .skip(1) - .filter_map(|line| { - let parts: Vec<&str> = line.split('/').collect(); - if !parts.is_empty() { - Some(Package { - provider: "apt/update".to_string(), - name: parts[0].to_string(), - version: "Update available".to_string(), - description: "".to_string(), - score: 1.0, - }) - } else { - None - } - }) - .collect() + let lines: Vec<&str> = stdout.lines().skip(1).collect(); + lines.par_iter().filter_map(|line| parse_update_line(line)).collect() } else { Vec::new() } @@ -192,3 +194,39 @@ impl PackageManager for AptManager { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_apt_search_lines_including_empty_descriptions() { + let with_description = parse_search_line("curl", "curl - command line tool").unwrap(); + assert_eq!(with_description.provider, "apt"); + assert_eq!(with_description.name, "curl"); + assert_eq!(with_description.description, "command line tool"); + + let without_description = parse_search_line("curl", "curl").unwrap(); + assert_eq!(without_description.name, "curl"); + assert_eq!(without_description.description, ""); + } + + #[test] + fn rejects_empty_apt_search_names() { + assert!(parse_search_line("curl", " - missing package").is_none()); + } + + #[test] + fn parses_apt_detail_and_update_lines() { + let detail = parse_installed_detail_line("curl\t8.5.0\ttransfer tool").unwrap(); + assert_eq!(detail.provider, "apt/local"); + assert_eq!(detail.name, "curl"); + assert_eq!(detail.version, "8.5.0"); + assert_eq!(detail.description, "transfer tool"); + + let update = parse_update_line("curl/stable 8.6.0 amd64 [upgradable]").unwrap(); + assert_eq!(update.provider, "apt/update"); + assert_eq!(update.name, "curl"); + assert_eq!(update.version, "Update available"); + } +} diff --git a/src/managers/mod.rs b/src/managers/mod.rs index 73eded9..2649ac4 100644 --- a/src/managers/mod.rs +++ b/src/managers/mod.rs @@ -5,6 +5,7 @@ pub mod pacman; pub mod yay; use ratatui::DefaultTerminal; +use rayon::prelude::*; use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; @@ -234,37 +235,54 @@ lazy_static::lazy_static! { } pub fn parse_alternating_lines(lines: &[&str], manager: String, query: &str) -> Vec { - let mut res = Vec::new(); - let mut i = 0; - - while i + 1 < lines.len() { - let first_line = lines[i]; - let second_line = lines[i + 1]; - - let parts: Vec<&str> = first_line.split_whitespace().collect(); + let mut res: Vec = lines + .par_chunks(2) + .filter_map(|chunk| { + let [first_line, second_line] = chunk else { + return None; + }; + + let parts: Vec<&str> = first_line.split_whitespace().collect(); + if parts.len() < 2 { + return None; + } - if parts.len() >= 2 { let package = parts[0].to_string(); let version = parts[1].to_string(); let description = second_line.trim().to_string(); - let package_name = package.split('/').next_back().unwrap_or(&package).to_string(); let score = crate::fuzzy::fuzzy_match(query, &package_name); - res.push(Package { - provider: manager.clone(), - name: package, - version, - description, - score, - }); - } - - i += 2; - } + Some(Package { provider: manager.clone(), name: package, version, description, score }) + }) + .collect(); res.retain(|p| p.score > 0.01); res.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); res } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_alternating_package_output_in_parallel_safe_chunks() { + let lines = [ + "extra/ripgrep 14.1.1-1", + " fast recursive search", + "core/grep 3.11-1", + " GNU grep", + "dangling-line-without-description 1.0.0", + ]; + + let packages = parse_alternating_lines(&lines, "pacman".to_string(), "ripgrep"); + + assert_eq!(packages.len(), 1); + assert_eq!(packages[0].provider, "pacman"); + assert_eq!(packages[0].name, "extra/ripgrep"); + assert_eq!(packages[0].version, "14.1.1-1"); + assert_eq!(packages[0].description, "fast recursive search"); + } +} diff --git a/src/managers/pacman.rs b/src/managers/pacman.rs index bf0adfc..bf59e2e 100644 --- a/src/managers/pacman.rs +++ b/src/managers/pacman.rs @@ -1,6 +1,7 @@ use super::Package; use crate::execute_external_command; use ratatui::DefaultTerminal; +use rayon::prelude::*; use std::collections::{HashMap, HashSet}; pub fn search_pacman(search_word: &str) -> Vec { @@ -129,8 +130,9 @@ pub fn get_installed_packages() -> HashSet { if let Some(output) = output { let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .lines() + let lines: Vec<&str> = stdout.lines().collect(); + lines + .par_iter() .filter_map(|line| line.split_whitespace().next().map(|s| s.to_string())) .collect() } else { @@ -143,8 +145,9 @@ pub fn get_installed_packages_details() -> Vec { if let Some(output) = output { let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .lines() + let lines: Vec<&str> = stdout.lines().collect(); + lines + .par_iter() .filter_map(|line| { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 2 { @@ -170,8 +173,9 @@ pub fn get_updates() -> Vec { if let Some(output) = output { let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .lines() + let lines: Vec<&str> = stdout.lines().collect(); + lines + .par_iter() .filter_map(|line| { // format: pkgname oldver -> newver let parts: Vec<&str> = line.split_whitespace().collect();