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
|
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;
|
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`.
|
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):
|
Gotchas hit (also see §6/§12):
|
||||||
- `slint::Result` / `slint::WindowHandle` **do not exist in 1.18.1** as
|
- `slint::Result` / `slint::WindowHandle` **do not exist in 1.18.1** as
|
||||||
published on crates.io — desktop bin is `fn main() -> Result<(),
|
published on crates.io — desktop bin is `fn main() -> Result<(),
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::scan::Entry;
|
use crate::scan::Entry;
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
const ANDROID_PACKAGE: &str = "wow.gmp.gallery";
|
const ANDROID_PACKAGE: &str = "wow.gmp.gallery";
|
||||||
|
|
||||||
/// App-writable data dir on Android. Prefers the per-app external dir
|
/// 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};
|
use slint::{ComponentHandle, Model, SharedString, VecModel};
|
||||||
|
|
||||||
/// M1: kick off the background scan + DB refresh; UI updates are posted to
|
/// Fill the window's list model from `paths` and set the status line.
|
||||||
/// the event loop (the scan thread must never touch the window directly).
|
fn set_list(weak: &slint::Weak<Main>, paths: Vec<String>, status: String) {
|
||||||
/// Called from both the desktop and Android entry points after `Main::new()`.
|
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) {
|
pub fn start_scan(window: &Main) {
|
||||||
let model = slint::ModelRc::new(VecModel::<SharedString>::default());
|
let model = slint::ModelRc::new(VecModel::<SharedString>::default());
|
||||||
window.set_items(model);
|
window.set_items(model);
|
||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
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()) {
|
let mut db = match db::open(&db::db_path()) {
|
||||||
Ok(db) => db,
|
Ok(db) => db,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("gallery: db error: {e}");
|
eprintln!("gallery: db error: {e}");
|
||||||
let _ = slint::invoke_from_event_loop( move || {
|
set_list(&weak, Vec::new(), format!("db error: {e}"));
|
||||||
if let Some(w) = weak.upgrade() {
|
|
||||||
w.set_status(format!("db error: {e}").into());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
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| {
|
let (ins, upd, del) = db::apply(&mut db, &entries).unwrap_or_else(|e| {
|
||||||
eprintln!("gallery: db apply error: {e}");
|
eprintln!("gallery: db apply error: {e}");
|
||||||
(0, 0, 0)
|
(0, 0, 0)
|
||||||
|
|
@ -47,14 +84,7 @@ pub fn start_scan(window: &Main) {
|
||||||
let ms = t0.elapsed().as_millis();
|
let ms = t0.elapsed().as_millis();
|
||||||
log::info!("gallery: scan {n} files ({ins}+ {upd}~ {del}-) in {ms} ms");
|
log::info!("gallery: scan {n} files ({ins}+ {upd}~ {del}-) in {ms} ms");
|
||||||
|
|
||||||
let weak = weak.clone();
|
set_list(&weak, paths, format!("{n} files ({ins}+ {upd}~ {del}–) in {ms} ms"));
|
||||||
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());
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
20
src/scan.rs
20
src/scan.rs
|
|
@ -43,24 +43,36 @@ pub fn roots() -> Vec<PathBuf> {
|
||||||
/// Recursively collect all image entries below `roots`.
|
/// Recursively collect all image entries below `roots`.
|
||||||
/// `on_progress(n)` is called every 512 files found.
|
/// `on_progress(n)` is called every 512 files found.
|
||||||
pub fn walk(roots: &[PathBuf], on_progress: &dyn Fn(usize)) -> Vec<Entry> {
|
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();
|
let mut out = Vec::new();
|
||||||
for r in roots {
|
for r in roots {
|
||||||
if r.is_dir() {
|
if r.is_dir() {
|
||||||
walk_dir(r, &mut out, on_progress);
|
walk_dir(r, &mut out, on_batch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
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 };
|
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
||||||
for ent in rd.flatten() {
|
for ent in rd.flatten() {
|
||||||
let p = ent.path();
|
let p = ent.path();
|
||||||
let Ok(ft) = ent.file_type() else { continue };
|
let Ok(ft) = ent.file_type() else { continue };
|
||||||
if ft.is_dir() {
|
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).
|
// Don't follow symlinked dirs (loop protection).
|
||||||
if !p.is_symlink() {
|
if !p.is_symlink() {
|
||||||
walk_dir(&p, out, on_progress);
|
walk_dir(&p, out, on_batch);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +96,7 @@ fn walk_dir(dir: &Path, out: &mut Vec<Entry>, on_progress: &dyn Fn(usize)) {
|
||||||
size: md.len() as i64,
|
size: md.len() as i64,
|
||||||
});
|
});
|
||||||
if out.len() % 512 == 0 {
|
if out.len() % 512 == 0 {
|
||||||
on_progress(out.len());
|
on_batch(out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user