mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-24 07:45:57 +02:00
Parallelize notebook and note scanning for faster loads on Android
This commit is contained in:
@@ -52,11 +52,10 @@ pub fn scan_notebooks(vault_path: &str) -> Result<Vec<NotebookEntry>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn scan_dir_recursive(dir: &Path, vault_root: &str) -> Vec<NotebookEntry> {
|
fn scan_dir_recursive(dir: &Path, vault_root: &str) -> Vec<NotebookEntry> {
|
||||||
let mut entries = Vec::new();
|
|
||||||
let root = Path::new(vault_root);
|
let root = Path::new(vault_root);
|
||||||
|
|
||||||
let Ok(read_dir) = fs::read_dir(dir) else {
|
let Ok(read_dir) = fs::read_dir(dir) else {
|
||||||
return entries;
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Collect subdirs (root level only lists directories, note counts come from scan_dir_with_count)
|
// Collect subdirs (root level only lists directories, note counts come from scan_dir_with_count)
|
||||||
@@ -76,29 +75,29 @@ fn scan_dir_recursive(dir: &Path, vault_root: &str) -> Vec<NotebookEntry> {
|
|||||||
.cmp(&b.file_name().to_string_lossy().to_lowercase())
|
.cmp(&b.file_name().to_string_lossy().to_lowercase())
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store note_count for the parent directory (used when this is the top-level call)
|
// Scan sibling notebooks in parallel: each subtree is independent, so
|
||||||
// Each directory pushes its own entry with its own note_count
|
// overlapping the per-directory read_dir calls hides FUSE latency on Android.
|
||||||
for dir_entry in dirs {
|
// par_iter().collect() preserves the (already-sorted) order.
|
||||||
let path = dir_entry.path();
|
let paths: Vec<PathBuf> = dirs.iter().map(|e| e.path()).collect();
|
||||||
let name = dir_entry.file_name().to_string_lossy().to_string();
|
paths
|
||||||
let relative = path
|
.par_iter()
|
||||||
.strip_prefix(root)
|
.map(|path| {
|
||||||
.unwrap_or(&path)
|
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||||
.to_string_lossy()
|
let relative = path
|
||||||
.to_string();
|
.strip_prefix(root)
|
||||||
|
.unwrap_or(path)
|
||||||
let (children, child_note_count) = scan_dir_with_count(&path, vault_root);
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
entries.push(NotebookEntry {
|
let (children, child_note_count) = scan_dir_with_count(path, vault_root);
|
||||||
name,
|
NotebookEntry {
|
||||||
path: path.to_string_lossy().to_string(),
|
name,
|
||||||
relative_path: relative,
|
path: path.to_string_lossy().to_string(),
|
||||||
children,
|
relative_path: relative,
|
||||||
note_count: child_note_count,
|
children,
|
||||||
});
|
note_count: child_note_count,
|
||||||
}
|
}
|
||||||
|
})
|
||||||
entries
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Combined scan: returns (children, note_count) in a single read_dir pass.
|
/// Combined scan: returns (children, note_count) in a single read_dir pass.
|
||||||
@@ -130,26 +129,26 @@ fn scan_dir_with_count(dir: &Path, vault_root: &str) -> (Vec<NotebookEntry>, usi
|
|||||||
.cmp(&b.file_name().to_string_lossy().to_lowercase())
|
.cmp(&b.file_name().to_string_lossy().to_lowercase())
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
let paths: Vec<PathBuf> = subdirs.iter().map(|e| e.path()).collect();
|
||||||
for dir_entry in subdirs {
|
let entries: Vec<NotebookEntry> = paths
|
||||||
let path = dir_entry.path();
|
.par_iter()
|
||||||
let name = dir_entry.file_name().to_string_lossy().to_string();
|
.map(|path| {
|
||||||
let relative = path
|
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||||
.strip_prefix(root)
|
let relative = path
|
||||||
.unwrap_or(&path)
|
.strip_prefix(root)
|
||||||
.to_string_lossy()
|
.unwrap_or(path)
|
||||||
.to_string();
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
let (children, child_note_count) = scan_dir_with_count(&path, vault_root);
|
let (children, child_note_count) = scan_dir_with_count(path, vault_root);
|
||||||
|
NotebookEntry {
|
||||||
entries.push(NotebookEntry {
|
name,
|
||||||
name,
|
path: path.to_string_lossy().to_string(),
|
||||||
path: path.to_string_lossy().to_string(),
|
relative_path: relative,
|
||||||
relative_path: relative,
|
children,
|
||||||
children,
|
note_count: child_note_count,
|
||||||
note_count: child_note_count,
|
}
|
||||||
});
|
})
|
||||||
}
|
.collect();
|
||||||
|
|
||||||
(entries, note_count)
|
(entries, note_count)
|
||||||
}
|
}
|
||||||
@@ -197,47 +196,36 @@ pub fn scan_notes(vault_path: &str, notebook_path: Option<&str>) -> Result<Vec<N
|
|||||||
// On mobile, use metadata-only scan (no file reads) for fast listing on sandboxed FS
|
// On mobile, use metadata-only scan (no file reads) for fast listing on sandboxed FS
|
||||||
#[cfg(mobile)]
|
#[cfg(mobile)]
|
||||||
{
|
{
|
||||||
let mut notes = Vec::new();
|
// Collect candidate .md paths first, then read their metadata in parallel.
|
||||||
// Log what read_dir sees
|
// Overlapping the per-file reads hides FUSE latency on Android (matches the
|
||||||
if let Ok(rd) = fs::read_dir(root) {
|
// desktop path below). The previous version also did an extra read_dir purely
|
||||||
let items: Vec<_> = rd.filter_map(|e| e.ok()).collect();
|
// for debug logging, which doubled the directory I/O on the sandboxed FS.
|
||||||
log::info!("scan_notes: read_dir found {} entries in {:?}", items.len(), root);
|
let paths: Vec<PathBuf> = if notebook_path.is_some() {
|
||||||
for (i, entry) in items.iter().enumerate().take(5) {
|
match fs::read_dir(root) {
|
||||||
let ft = entry.file_type().map(|ft| if ft.is_file() { "file" } else if ft.is_dir() { "dir" } else { "other" }).unwrap_or("err");
|
Ok(rd) => rd
|
||||||
log::info!(" [{}] {:?} type={}", i, entry.file_name(), ft);
|
.filter_map(|e| e.ok())
|
||||||
}
|
.map(|e| e.path())
|
||||||
} else {
|
.filter(|p| p.is_file() && p.extension().and_then(|x| x.to_str()) == Some("md"))
|
||||||
log::info!("scan_notes: read_dir FAILED for {:?}", root);
|
.collect(),
|
||||||
}
|
Err(_) => Vec::new(),
|
||||||
|
|
||||||
if notebook_path.is_some() {
|
|
||||||
if let Ok(rd) = fs::read_dir(root) {
|
|
||||||
for entry in rd.filter_map(|e| e.ok()) {
|
|
||||||
let is_file = entry.file_type().map(|ft| ft.is_file()).unwrap_or(false);
|
|
||||||
if is_file && entry.path().extension().and_then(|x| x.to_str()) == Some("md") {
|
|
||||||
if let Ok(note) = read_note_entry_metadata_only(&entry.path(), vault_root) {
|
|
||||||
notes.push(note);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let hn_dir = helixnotes_dir(vault_path);
|
let hn_dir = helixnotes_dir(vault_path);
|
||||||
for entry in WalkDir::new(root)
|
WalkDir::new(root)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// filter_entry skips descending into hidden dirs (unlike filter)
|
// filter_entry skips descending into hidden dirs (unlike filter)
|
||||||
.filter_entry(|e| !is_hidden(e.path()) && !e.path().starts_with(&hn_dir))
|
.filter_entry(|e| !is_hidden(e.path()) && !e.path().starts_with(&hn_dir))
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
.filter(|e| {
|
.map(|e| e.path().to_path_buf())
|
||||||
e.file_type().is_file()
|
.filter(|p| p.is_file() && p.extension().and_then(|x| x.to_str()) == Some("md"))
|
||||||
&& e.path().extension().and_then(|x| x.to_str()) == Some("md")
|
.collect()
|
||||||
})
|
};
|
||||||
{
|
|
||||||
if let Ok(note) = read_note_entry_metadata_only(entry.path(), vault_root) {
|
let mut notes: Vec<NoteEntry> = paths
|
||||||
notes.push(note);
|
.par_iter()
|
||||||
}
|
.filter_map(|path| read_note_entry_metadata_only(path, vault_root).ok())
|
||||||
}
|
.collect();
|
||||||
}
|
|
||||||
log::info!("scan_notes: mobile scan found {} notes", notes.len());
|
log::info!("scan_notes: mobile scan found {} notes", notes.len());
|
||||||
notes.sort_by(|a, b| b.meta.modified.cmp(&a.meta.modified));
|
notes.sort_by(|a, b| b.meta.modified.cmp(&a.meta.modified));
|
||||||
return Ok(notes);
|
return Ok(notes);
|
||||||
|
|||||||
Reference in New Issue
Block a user