perf(engine): #1136 album order and album commands without per-photo scans

A manual album shown in its own order sorted with position() per photo
(O(n^2): 2.0 s for 20k photos, now 18 ms). album.addPhotos, removePhotos
and toggleTarget test membership with sets. The 100k scale test measures
all three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chad Elliott
2026-10-11 16:48:16 -05:00
co-authored by Claude Opus 5.5
parent 615d8a365e
commit 3f3ffa541a
3 changed files with 30 additions and 5 deletions
+9 -3
View File
@@ -1147,8 +1147,10 @@ pub fn specs() -> Vec<CommandSpec> {
let (targets, skipped) = album_targets_or_err(s, "album.addPhotos", targets)?;
let mut photos = al.photos.clone();
let before = photos.len();
// looked up, not searched for: adding thousands to a big album stays linear
let mut held: std::collections::HashSet<PhotoId> = photos.iter().copied().collect();
for t in targets {
if !photos.contains(&t) {
if held.insert(t) {
photos.push(t);
}
}
@@ -1179,11 +1181,14 @@ pub fn specs() -> Vec<CommandSpec> {
}
};
let al = s.catalog.album(id).ok_or_else(|| bad("album.toggleTarget", "no target album"))?;
let all_in = !targets.is_empty() && targets.iter().all(|t| al.photos.contains(t));
// looked up, not searched for: thousands toggled in a big album stay linear
let mut held: std::collections::HashSet<PhotoId> = al.photos.iter().copied().collect();
let all_in = !targets.is_empty() && targets.iter().all(|t| held.contains(t));
let photos: Vec<PhotoId> = if all_in {
let targets: std::collections::HashSet<PhotoId> = targets.iter().copied().collect();
al.photos.iter().copied().filter(|x| !targets.contains(x)).collect()
} else {
al.photos.iter().copied().chain(targets.iter().copied().filter(|t| !al.photos.contains(t))).collect()
al.photos.iter().copied().chain(targets.iter().copied().filter(|t| held.insert(*t))).collect()
};
let name = al.name.clone();
let cover = al.cover.filter(|c| photos.contains(c)).or(photos.first().copied());
@@ -1317,6 +1322,7 @@ pub fn specs() -> Vec<CommandSpec> {
if al.is_smart() {
return Err(bad("album.removePhotos", "smart albums update automatically: change their rules"));
}
let targets: std::collections::HashSet<PhotoId> = targets.into_iter().collect();
let photos: Vec<PhotoId> = al.photos.iter().copied().filter(|x| !targets.contains(x)).collect();
s.commit("Remove from Album", Op::SetAlbumPhotos { id, photos })?;
ok()
+6 -2
View File
@@ -874,8 +874,12 @@ impl Session {
&& !al.is_smart()
&& self.filter == Filter::default()
{
let order = al.photos.clone();
visible.sort_by_key(|id| order.iter().position(|x| x == id).unwrap_or(usize::MAX));
// each photo's place looked up, not searched for (a big album sorted in O(n log n))
let mut place: std::collections::HashMap<PhotoId, usize> = std::collections::HashMap::with_capacity(al.photos.len());
for (i, id) in al.photos.iter().enumerate() {
place.entry(*id).or_insert(i);
}
visible.sort_by_key(|id| place.get(id).copied().unwrap_or(usize::MAX));
if !self.sort.ascending {
visible.reverse();
}
+15
View File
@@ -440,6 +440,21 @@ fn scale_100k_library_queries() {
m("undo", &mut || {
s.execute("edit.undo", &serde_json::json!({})).unwrap();
});
// a 20k-photo album shown in its own order (capture-date sort, no filter)
let big = s.execute("album.create", &serde_json::json!({"name": "Big"})).unwrap()["id"].as_u64().unwrap();
let ids: Vec<u64> = (0..n).rev().step_by(5).map(|i| i + 1).collect();
m("add 20k to an album", &mut || {
s.execute("album.addPhotos", &serde_json::json!({"id": big, "ids": ids})).unwrap();
});
m("album of 20k (album order)", &mut || {
s.execute("library.source", &serde_json::json!({"kind": "album", "id": big})).unwrap();
let _ = s.visible_cloned();
});
s.execute("library.source", &serde_json::json!({"kind": "all"})).unwrap();
let half: Vec<u64> = ids.iter().copied().step_by(2).collect();
m("remove 10k from the album", &mut || {
s.execute("album.removePhotos", &serde_json::json!({"id": big, "ids": half})).unwrap();
});
worst.sort_by(|a, b| b.1.total_cmp(&a.1));
eprintln!("slowest: {:?}", &worst[..3]);
}