Parallelize vault-stats and tasks scanning for faster Info/Tasks loads on Android

This commit is contained in:
Yuri Karamian
2026-06-25 12:37:32 +02:00
parent 761bf5b42e
commit 30c3523722
+44 -46
View File
@@ -664,6 +664,7 @@ fn extract_title_fast(raw: &str) -> Option<String> {
#[tauri::command] #[tauri::command]
pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskItem>, String> { pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskItem>, String> {
use rayon::prelude::*;
let config = state.config.lock().map_err(|e| e.to_string())?; let config = state.config.lock().map_err(|e| e.to_string())?;
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?; let vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
let vault = std::path::Path::new(vault_path); let vault = std::path::Path::new(vault_path);
@@ -673,46 +674,44 @@ pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskIte
let due_re = regex::Regex::new(r"\bdue:(\d{4}-\d{2}-\d{2})\b").unwrap(); let due_re = regex::Regex::new(r"\bdue:(\d{4}-\d{2}-\d{2})\b").unwrap();
let prio_re = regex::Regex::new(r"(?i)(?:^|\s)!(high|medium|med|low)\b").unwrap(); let prio_re = regex::Regex::new(r"(?i)(?:^|\s)!(high|medium|med|low)\b").unwrap();
let mut tasks = Vec::new(); let paths: Vec<std::path::PathBuf> = walkdir::WalkDir::new(vault)
for entry in walkdir::WalkDir::new(vault)
.into_iter() .into_iter()
.filter_entry(|e| !operations::is_hidden(e.path()) && !e.path().starts_with(&hn_dir)) .filter_entry(|e| !operations::is_hidden(e.path()) && !e.path().starts_with(&hn_dir))
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
{ .filter(|e| {
let path = entry.path(); e.file_type().is_file() && e.path().extension().and_then(|x| x.to_str()) == Some("md")
if !path.is_file() { })
continue; .map(|e| e.into_path())
} .collect();
if path.extension().and_then(|e| e.to_str()) != Some("md") {
continue; // Read and parse each note in parallel; on Android every read_to_string is a FUSE round-trip.
} let tasks: Vec<crate::types::TaskItem> = paths
.par_iter()
.flat_map(|path| {
let mut out: Vec<crate::types::TaskItem> = Vec::new();
let raw = match std::fs::read_to_string(path) { let raw = match std::fs::read_to_string(path) {
Ok(r) => r, Ok(r) => r,
Err(_) => continue, Err(_) => return out,
}; };
let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string(); let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string();
let (meta, body) = crate::vault::frontmatter::parse_note(&raw, &filename); let (meta, body) = crate::vault::frontmatter::parse_note(&raw, &filename);
let note_path = path.to_string_lossy().to_string(); let note_path = path.to_string_lossy().to_string();
for (i, line) in body.lines().enumerate() { for (i, line) in body.lines().enumerate() {
let caps = match task_re.captures(line) { let caps = match task_re.captures(line) {
Some(c) => c, Some(c) => c,
None => continue, None => continue,
}; };
let completed = !caps[1].trim().is_empty(); // 'x'/'X' completed, ' ' open let completed = !caps[1].trim().is_empty();
let content = caps[2].to_string(); let content = caps[2].to_string();
let due = due_re.captures(&content).map(|c| c[1].to_string()); let due = due_re.captures(&content).map(|c| c[1].to_string());
let priority = prio_re.captures(&content).map(|c| { let priority = prio_re.captures(&content).map(|c| {
let p = c[1].to_lowercase(); let p = c[1].to_lowercase();
if p == "medium" { "med".to_string() } else { p } if p == "medium" { "med".to_string() } else { p }
}); });
// Strip metadata tokens for the display text, then collapse whitespace.
let mut text = due_re.replace_all(&content, "").to_string(); let mut text = due_re.replace_all(&content, "").to_string();
text = prio_re.replace_all(&text, " ").to_string(); text = prio_re.replace_all(&text, " ").to_string();
let text = text.split_whitespace().collect::<Vec<_>>().join(" "); let text = text.split_whitespace().collect::<Vec<_>>().join(" ");
out.push(crate::types::TaskItem {
tasks.push(crate::types::TaskItem {
note_path: note_path.clone(), note_path: note_path.clone(),
note_title: meta.title.clone(), note_title: meta.title.clone(),
line: i, line: i,
@@ -723,7 +722,9 @@ pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskIte
priority, priority,
}); });
} }
} out
})
.collect();
Ok(tasks) Ok(tasks)
} }
@@ -1223,41 +1224,38 @@ pub fn reorder_quick_access(state: State<'_, AppState>, paths: Vec<String>) -> R
#[tauri::command] #[tauri::command]
pub fn get_vault_stats(state: State<'_, AppState>) -> Result<VaultStats, String> { pub fn get_vault_stats(state: State<'_, AppState>) -> Result<VaultStats, String> {
use rayon::prelude::*;
let config = state.config.lock().map_err(|e| e.to_string())?; let config = state.config.lock().map_err(|e| e.to_string())?;
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?; let vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
let mut total_notes: u64 = 0; let paths: Vec<std::path::PathBuf> = walkdir::WalkDir::new(vault_path)
let mut total_attachments: u64 = 0;
let mut notes_size: u64 = 0;
let mut attachments_size: u64 = 0;
for entry in walkdir::WalkDir::new(vault_path)
.into_iter() .into_iter()
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
{ .filter(|e| e.file_type().is_file())
let path = entry.path(); .map(|e| e.into_path())
if !path.is_file() { .filter(|p| {
continue; // attachments under .helixnotes count; nothing else in .helixnotes or .trash does
} let s = p.to_string_lossy();
let path_str = path.to_string_lossy(); !(s.contains("/.helixnotes/") && !s.contains("/.helixnotes/attachments/"))
// Include .helixnotes/attachments, skip everything else in .helixnotes && !s.contains("/.trash/")
if path_str.contains("/.helixnotes/") && !path_str.contains("/.helixnotes/attachments/") { })
continue; .collect();
}
// Skip trash // Stat files in parallel; each metadata() is a FUSE round-trip on Android.
if path_str.contains("/.trash/") { let (total_notes, total_attachments, notes_size, attachments_size) = paths
continue; .par_iter()
} .map(|path| {
let size = entry.metadata().map(|m| m.len()).unwrap_or(0); let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); if path.extension().and_then(|e| e.to_str()) == Some("md") {
if ext == "md" { (1u64, 0u64, size, 0u64)
total_notes += 1;
notes_size += size;
} else { } else {
total_attachments += 1; (0u64, 1u64, 0u64, size)
attachments_size += size;
}
} }
})
.reduce(
|| (0u64, 0u64, 0u64, 0u64),
|a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3),
);
Ok(VaultStats { Ok(VaultStats {
total_notes, total_notes,