M1: instant list paint + streaming first scan
- relaunch: DB list paints immediately ('N files (cached)'), background
walk refreshes ('Updating… N') — no more 5.5 s blank list on warm start
- first launch: walk_stream() feeds the list in 512-file batches while
scanning; UI never waits for the full DB build
- skip hidden dirs (.thumbnails/.trash/…): -559 entries on the phone
- db.rs: cfg-gate ANDROID_PACKAGE (dead-code warning on desktop)
Phone (8.5k photos): cold scan 4.2 s with live list; warm relaunch
paints instantly, rescan 4.2 s in background.
This commit is contained in:
parent
e1982eade4
commit
c72b00f7ec
|
|
@ -273,6 +273,15 @@ pool threads), and `logcat` pool timing logs behind a `debug_timing` flag.
|
|||
thread → sqlite → `VecModel` in a `ListView` (paths as text). Acceptance
|
||||
verified on emulator: **5002 files in 153 ms**; relaunch `0+ 0~ 0-` in 2 ms;
|
||||
mtime-touch → `1~`, delete → `1-`. Status line shows `N files (a+ u~ d–) in X ms`.
|
||||
List paint is instant on relaunch: the DB list is shown first
|
||||
("N files (cached)"), the background walk then runs and refreshes
|
||||
("Updating… N"). First launch streams results into the list in 512-file
|
||||
batches while scanning — the UI never waits for the full DB build
|
||||
(`walk_stream`). Hidden dirs (`.thumbnails`, …) are skipped.
|
||||
Real-device numbers (Pixel-class, 8.5k photos): cold scan ~4.2–5.9 s
|
||||
(filesystem metadata cost — the walk is the bottleneck, not sqlite),
|
||||
relaunch paint instant, rescan ~4.2 s. Walk parallelism is an M2/M7
|
||||
optimization candidate.
|
||||
Gotchas hit (also see §6/§12):
|
||||
- `slint::Result` / `slint::WindowHandle` **do not exist in 1.18.1** as
|
||||
published on crates.io — desktop bin is `fn main() -> Result<(),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use std::path::PathBuf;
|
|||
|
||||
use crate::scan::Entry;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
const ANDROID_PACKAGE: &str = "wow.gmp.gallery";
|
||||
|
||||
/// App-writable data dir on Android. Prefers the per-app external dir
|
||||
|
|
|
|||
86
src/lib.rs
86
src/lib.rs
|
|
@ -5,39 +5,76 @@ pub mod scan;
|
|||
|
||||
use slint::{ComponentHandle, Model, SharedString, VecModel};
|
||||
|
||||
/// M1: kick off the background scan + DB refresh; UI updates are posted to
|
||||
/// the event loop (the scan thread must never touch the window directly).
|
||||
/// Called from both the desktop and Android entry points after `Main::new()`.
|
||||
/// Fill the window's list model from `paths` and set the status line.
|
||||
fn set_list(weak: &slint::Weak<Main>, paths: Vec<String>, status: String) {
|
||||
let weak = weak.clone();
|
||||
let _ = slint::invoke_from_event_loop(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(m) = w.get_items().as_any().downcast_ref::<VecModel<SharedString>>() {
|
||||
m.set_vec(paths.into_iter().map(SharedString::from).collect::<Vec<_>>());
|
||||
}
|
||||
w.set_status(status.into());
|
||||
});
|
||||
}
|
||||
|
||||
/// Set only the status line (never touches the model).
|
||||
fn set_status(weak: &slint::Weak<Main>, status: String) {
|
||||
let weak = weak.clone();
|
||||
let _ = slint::invoke_from_event_loop(move || {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_status(status.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// M1: metadata from sqlite paints the list immediately (first frame < 100 ms
|
||||
/// per HANDOFF §8); a background walk then refreshes the DB and the list.
|
||||
/// UI updates are posted to the event loop — the scan thread never touches the
|
||||
/// window directly. Called from both entry points after `Main::new()`.
|
||||
pub fn start_scan(window: &Main) {
|
||||
let model = slint::ModelRc::new(VecModel::<SharedString>::default());
|
||||
window.set_items(model);
|
||||
let weak = window.as_weak();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let t0 = std::time::Instant::now();
|
||||
let roots = scan::roots();
|
||||
let entries = scan::walk(&roots, &|n| {
|
||||
let weak = weak.clone();
|
||||
let msg = format!("Scanning… {}", n);
|
||||
let _ = slint::invoke_from_event_loop(move || {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_status(msg.into());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let mut db = match db::open(&db::db_path()) {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
eprintln!("gallery: db error: {e}");
|
||||
let _ = slint::invoke_from_event_loop( move || {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
w.set_status(format!("db error: {e}").into());
|
||||
}
|
||||
});
|
||||
set_list(&weak, Vec::new(), format!("db error: {e}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Instant first paint from the cached DB (relaunch path).
|
||||
let cached = db::list_paths(&mut db).unwrap_or_default();
|
||||
let n0 = cached.len();
|
||||
let db_warm = n0 > 0;
|
||||
if db_warm {
|
||||
set_list(&weak, cached, format!("{n0} files (cached)"));
|
||||
}
|
||||
|
||||
// 2. Background rescan: detect new/changed/removed files.
|
||||
// Cold start: stream results into the list as they're found, so the
|
||||
// UI fills in while the scan runs (never waits for the full DB).
|
||||
// Warm start: keep the cached list, only update the status line.
|
||||
let t0 = std::time::Instant::now();
|
||||
let roots = scan::roots();
|
||||
let entries = scan::walk_stream(&roots, &|batch| {
|
||||
let n = batch.len();
|
||||
if db_warm {
|
||||
set_status(&weak, format!("Updating… {n}"));
|
||||
} else {
|
||||
let mut sorted = batch.to_vec();
|
||||
sorted.sort_by(|a, b| b.mtime.cmp(&a.mtime).then_with(|| a.path.cmp(&b.path)));
|
||||
set_list(
|
||||
&weak,
|
||||
sorted.into_iter().map(|e| e.path).collect(),
|
||||
format!("Scanning… {n}"),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let (ins, upd, del) = db::apply(&mut db, &entries).unwrap_or_else(|e| {
|
||||
eprintln!("gallery: db apply error: {e}");
|
||||
(0, 0, 0)
|
||||
|
|
@ -47,14 +84,7 @@ pub fn start_scan(window: &Main) {
|
|||
let ms = t0.elapsed().as_millis();
|
||||
log::info!("gallery: scan {n} files ({ins}+ {upd}~ {del}-) in {ms} ms");
|
||||
|
||||
let weak = weak.clone();
|
||||
let _ = slint::invoke_from_event_loop(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(m) = w.get_items().as_any().downcast_ref::<VecModel<SharedString>>() {
|
||||
m.set_vec(paths.into_iter().map(SharedString::from).collect::<Vec<_>>());
|
||||
}
|
||||
w.set_status(format!("{n} files ({ins}+ {upd}~ {del}–) in {ms} ms").into());
|
||||
});
|
||||
set_list(&weak, paths, format!("{n} files ({ins}+ {upd}~ {del}–) in {ms} ms"));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
20
src/scan.rs
20
src/scan.rs
|
|
@ -43,24 +43,36 @@ pub fn roots() -> Vec<PathBuf> {
|
|||
/// Recursively collect all image entries below `roots`.
|
||||
/// `on_progress(n)` is called every 512 files found.
|
||||
pub fn walk(roots: &[PathBuf], on_progress: &dyn Fn(usize)) -> Vec<Entry> {
|
||||
walk_stream(roots, &|batch| on_progress(batch.len()))
|
||||
}
|
||||
|
||||
/// Like `walk`, but streams progress: `on_batch(&entries)` receives ALL
|
||||
/// entries collected so far every 512 new files, so the UI can show results
|
||||
/// while the scan is still running (first launch must not wait for the full
|
||||
/// DB build).
|
||||
pub fn walk_stream(roots: &[PathBuf], on_batch: &dyn Fn(&[Entry])) -> Vec<Entry> {
|
||||
let mut out = Vec::new();
|
||||
for r in roots {
|
||||
if r.is_dir() {
|
||||
walk_dir(r, &mut out, on_progress);
|
||||
walk_dir(r, &mut out, on_batch);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn walk_dir(dir: &Path, out: &mut Vec<Entry>, on_progress: &dyn Fn(usize)) {
|
||||
fn walk_dir(dir: &Path, out: &mut Vec<Entry>, on_batch: &dyn Fn(&[Entry])) {
|
||||
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
||||
for ent in rd.flatten() {
|
||||
let p = ent.path();
|
||||
let Ok(ft) = ent.file_type() else { continue };
|
||||
if ft.is_dir() {
|
||||
// Skip hidden dirs (.thumbnails, .trash, …) — metadata, not photos.
|
||||
if p.file_name().and_then(|n| n.to_str()).is_some_and(|n| n.starts_with('.')) {
|
||||
continue;
|
||||
}
|
||||
// Don't follow symlinked dirs (loop protection).
|
||||
if !p.is_symlink() {
|
||||
walk_dir(&p, out, on_progress);
|
||||
walk_dir(&p, out, on_batch);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -84,7 +96,7 @@ fn walk_dir(dir: &Path, out: &mut Vec<Entry>, on_progress: &dyn Fn(usize)) {
|
|||
size: md.len() as i64,
|
||||
});
|
||||
if out.len() % 512 == 0 {
|
||||
on_progress(out.len());
|
||||
on_batch(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user