Fetch the photographs around the open one ahead of the step to them
Benchmarks / CPU and I/O (per commit) (push) Successful in 3m59s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / android-image (push) Canceled after 0s
🐳 Android image / Build and push (push) Canceled after 0s
Build and test / Android (aarch64) (push) Canceled after 0s
Build and test / windows-image (push) Canceled after 0s
🐳 Windows image / Build and push (push) Canceled after 0s
Build and test / Windows (x86_64, cross) (push) Canceled after 0s
Build and test / Layer separation (push) Canceled after 0s
Build and test / Desktop (Linux) (push) Canceled after 16m26s
Traceability / Requirement traces (push) Canceled after 0s
Benchmarks / CPU and I/O (per commit) (push) Successful in 3m59s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / android-image (push) Canceled after 0s
🐳 Android image / Build and push (push) Canceled after 0s
Build and test / Android (aarch64) (push) Canceled after 0s
Build and test / windows-image (push) Canceled after 0s
🐳 Windows image / Build and push (push) Canceled after 0s
Build and test / Windows (x86_64, cross) (push) Canceled after 0s
Build and test / Layer separation (push) Canceled after 0s
Build and test / Desktop (Linux) (push) Canceled after 16m26s
Traceability / Requirement traces (push) Canceled after 0s
Walking the photo roll was one download per frame: every step showed "Downloading…" over an empty canvas while tens of megabytes came down, and moving between a pair of near-identical frames paid that a dozen times. Now, once the opened photograph has landed, the ones around it are fetched into the originals cache while it is being looked at, so the next step is a disk read. A single worker serves the latest wish only, closest first and working outwards — next, previous, next-but-one, previous-but-one… — one file at a time. Each open replaces the wish, so a fast walk never leaves a trail of stale downloads competing with the one being waited on. A process-wide in-flight registry makes a click on a photograph that is still being fetched ahead wait for that transfer and read it from disk, rather than start a second download of the same file. How far each side is a setting under STORAGE — Off, 2, 5, 10 or 20, defaulting to 5 — and it is moot while "keep originals after opening" is off, since a fetch the cache would discard on arrival is transfer for nothing. Nothing is fetched ahead while offline. The transfers show in the activity list while they run and are removed when they end.
This commit is contained in:
@@ -384,6 +384,11 @@ pub struct LibraryController {
|
||||
/// small budget still keeps a working set. A metered or small-disk device
|
||||
/// wants the first.
|
||||
keep_opened: std::cell::Cell<bool>,
|
||||
/// TRACES: FR-NC-6a | FR-UI-4
|
||||
/// How many photographs each side of the open one are fetched ahead.
|
||||
/// Mirrors `CacheSettings::fetch_ahead`, held beside the flag that
|
||||
/// gates it.
|
||||
fetch_ahead: std::cell::Cell<u32>,
|
||||
/// TRACES: FR-CAT-13 | NFR-R4
|
||||
/// Whether judgements and keywords also go to the `.xmp` beside the
|
||||
/// original. Mirrors `LibrarySettings::write_xmp_sidecars`.
|
||||
@@ -474,6 +479,7 @@ impl LibraryController {
|
||||
keep_opened: std::cell::Cell::new(
|
||||
dr_types::CacheSettings::default().keep_opened_originals,
|
||||
),
|
||||
fetch_ahead: std::cell::Cell::new(dr_types::CacheSettings::default().fetch_ahead),
|
||||
write_xmp: std::cell::Cell::new(
|
||||
dr_types::LibrarySettings::default().write_xmp_sidecars,
|
||||
),
|
||||
@@ -516,6 +522,16 @@ impl LibraryController {
|
||||
self.keep_opened.set(keep);
|
||||
}
|
||||
|
||||
/// TRACES: FR-NC-6a | FR-UI-4
|
||||
/// How far along the roll, each side, the next open fetches ahead.
|
||||
pub fn set_fetch_ahead(&self, depth: u32) {
|
||||
self.fetch_ahead.set(depth);
|
||||
}
|
||||
|
||||
pub fn fetch_ahead(&self) -> u32 {
|
||||
self.fetch_ahead.get()
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-13 | NFR-R4
|
||||
pub fn set_write_xmp_sidecars(&self, on: bool) {
|
||||
self.write_xmp.set(on);
|
||||
@@ -583,6 +599,32 @@ impl LibraryController {
|
||||
.map(|id| dr_types::ImageId(*id as u64))
|
||||
}
|
||||
|
||||
/// TRACES: FR-NC-6a | FR-UI-4
|
||||
/// The photographs within `depth` of `path` on the roll, closest first
|
||||
/// and working outwards: next, previous, next-but-one, previous-but-one…
|
||||
///
|
||||
/// That order is the point. The prefetcher serves the list one at a time
|
||||
/// and drops the rest the moment the user moves, so whatever depth is
|
||||
/// set, the frame most likely to be stepped to is the first thing
|
||||
/// fetched — and the step most likely is one forward, which is why next
|
||||
/// leads previous at every distance.
|
||||
///
|
||||
/// In the roll's order, which is the loaded window's — the same rows a
|
||||
/// roll pick names — so what gets fetched ahead is exactly what a step
|
||||
/// lands on, filter and scope included. The window's edges cut the list
|
||||
/// short; a photograph not in the window has no neighbours, and a
|
||||
/// prefetch of nothing is the right answer for it.
|
||||
pub fn neighbours_of(&self, path: &str, depth: u32) -> Vec<String> {
|
||||
let paths = self.paths.borrow();
|
||||
let Some(at) = paths.iter().position(|p| p == path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
(1..=depth as usize)
|
||||
.flat_map(|d| [at.checked_add(d), at.checked_sub(d)])
|
||||
.filter_map(|i| paths.get(i?).cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// TRACES: FR-NC-8 | FR-DEV-6
|
||||
/// The default version's uuid for an image, by its remote path.
|
||||
///
|
||||
@@ -7919,6 +7961,39 @@ mod tests {
|
||||
assert_eq!(seen, vec![0, 1, 2, 3]);
|
||||
}
|
||||
|
||||
/// The prefetch order is the walking order: closest first, next before
|
||||
/// previous at every distance, and the window's edges cut it short.
|
||||
#[test]
|
||||
fn neighbours_are_listed_closest_first_working_outwards() {
|
||||
let ctl = LibraryController::new(crate::activity::ActivityLog::new());
|
||||
*ctl.paths.borrow_mut() = ["a", "b", "c", "d", "e", "f"].map(String::from).to_vec();
|
||||
|
||||
assert_eq!(ctl.neighbours_of("c", 2), vec!["d", "b", "e", "a"]);
|
||||
assert_eq!(
|
||||
ctl.neighbours_of("c", 10),
|
||||
vec!["d", "b", "e", "a", "f"],
|
||||
"past an edge the other side keeps going"
|
||||
);
|
||||
assert_eq!(
|
||||
ctl.neighbours_of("a", 1),
|
||||
vec!["b"],
|
||||
"nothing before the first"
|
||||
);
|
||||
assert_eq!(
|
||||
ctl.neighbours_of("f", 1),
|
||||
vec!["e"],
|
||||
"nothing after the last"
|
||||
);
|
||||
assert!(
|
||||
ctl.neighbours_of("c", 0).is_empty(),
|
||||
"zero fetches nothing ahead"
|
||||
);
|
||||
assert!(
|
||||
ctl.neighbours_of("z", 3).is_empty(),
|
||||
"a photograph outside the window has no neighbours to fetch"
|
||||
);
|
||||
}
|
||||
|
||||
/// A controller holding a catalog of four named images.
|
||||
fn with_catalog() -> Rc<LibraryController> {
|
||||
let ctl = LibraryController::new(crate::activity::ActivityLog::new());
|
||||
|
||||
Reference in New Issue
Block a user