Say a photograph is downloading, and how far, instead of failing
The develop view reported a remote original on its way through the error message, so it read "Could not load image" over "Downloading…". It did so on every step along the roll, including a cached frame that was ready within a tick, so each step flashed the error. Waiting is now its own state. On the step, the grid's thumbnail of the photograph stands in at once. Only when a transfer is really on the wire does it dim under "Not on this device yet", with a line like "Downloading — 12.4 of 38.0 MB" and a progress bar. The bytes come from a new RemoteBackend::get_reporting. The Nextcloud backend overrides it to read the body chunk by chunk; the default reports once at the end. Progress is kept in the in-flight registry by path, because a step usually lands on a frame the prefetcher is already fetching. The catalog's file length stands in when the server sends no Content-Length.
This commit is contained in:
@@ -588,9 +588,31 @@ pub fn spawn_full_fetch(
|
||||
/// because that is the one name every caller has.
|
||||
static IN_FLIGHT: std::sync::LazyLock<InFlight> = std::sync::LazyLock::new(InFlight::default);
|
||||
|
||||
/// TRACES: FR-NC-6a
|
||||
/// How far one original's transfer has got: bytes received, and the length
|
||||
/// the server declared (zero until it has, or if it never does).
|
||||
///
|
||||
/// Held in the registry beside the claim rather than handed to the caller,
|
||||
/// because the caller watching is often not the one downloading: a step along
|
||||
/// the roll usually lands on a frame the [`Prefetcher`] is already fetching,
|
||||
/// and the click waits on that transfer instead of starting its own.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Transfer {
|
||||
received: std::sync::atomic::AtomicU64,
|
||||
declared: std::sync::atomic::AtomicU64,
|
||||
}
|
||||
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Bytes received so far and bytes expected, for an original being fetched
|
||||
/// right now by anyone. `None` when nothing is fetching `path` — it is in the
|
||||
/// cache, or the transfer has not reached the network yet, or it has ended.
|
||||
pub fn transfer_progress(path: &str) -> Option<(u64, Option<u64>)> {
|
||||
IN_FLIGHT.progress(path)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct InFlight {
|
||||
busy: std::sync::Mutex<std::collections::HashSet<String>>,
|
||||
busy: std::sync::Mutex<std::collections::HashMap<String, std::sync::Arc<Transfer>>>,
|
||||
freed: std::sync::Condvar,
|
||||
/// Threads parked in [`InFlight::claim`], counted under the lock so a
|
||||
/// test can release the holder only once a waiter is really waiting.
|
||||
@@ -607,21 +629,32 @@ impl InFlight {
|
||||
/// stored what the caller was about to download.
|
||||
fn claim(&self, path: &str) -> Option<InFlightGuard<'_>> {
|
||||
let mut busy = self.busy.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if busy.insert(path.to_string()) {
|
||||
if !busy.contains_key(path) {
|
||||
let transfer = std::sync::Arc::new(Transfer::default());
|
||||
busy.insert(path.to_string(), transfer.clone());
|
||||
return Some(InFlightGuard {
|
||||
of: self,
|
||||
path: path.to_string(),
|
||||
transfer,
|
||||
});
|
||||
}
|
||||
#[cfg(test)]
|
||||
self.waiting
|
||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
while busy.contains(path) {
|
||||
while busy.contains_key(path) {
|
||||
busy = self.freed.wait(busy).unwrap_or_else(|e| e.into_inner());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn progress(&self, path: &str) -> Option<(u64, Option<u64>)> {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
let busy = self.busy.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let t = busy.get(path)?;
|
||||
let declared = t.declared.load(Relaxed);
|
||||
Some((t.received.load(Relaxed), (declared > 0).then_some(declared)))
|
||||
}
|
||||
|
||||
fn release(&self, path: &str) {
|
||||
self.busy
|
||||
.lock()
|
||||
@@ -636,6 +669,7 @@ impl InFlight {
|
||||
pub(super) struct InFlightGuard<'a> {
|
||||
of: &'a InFlight,
|
||||
path: String,
|
||||
transfer: std::sync::Arc<Transfer>,
|
||||
}
|
||||
|
||||
impl Drop for InFlightGuard<'_> {
|
||||
@@ -678,7 +712,7 @@ pub(super) fn fetch_original(
|
||||
|
||||
// Miss, claim, and if the claim had to wait, look again: the thread that
|
||||
// held the path has finished with it, and what it fetched is on disk.
|
||||
let _claim = loop {
|
||||
let claim = loop {
|
||||
if let Some(bytes) = from_cache() {
|
||||
return Ok(bytes);
|
||||
}
|
||||
@@ -693,7 +727,14 @@ pub(super) fn fetch_original(
|
||||
let backend = crate::remote::connect(&conn).map_err(FetchFailure::local)?;
|
||||
|
||||
let id = RemoteId::Path(RemotePath::new(path));
|
||||
let bytes = backend.get(&id, None).await?;
|
||||
let transfer = &claim.transfer;
|
||||
let bytes = backend
|
||||
.get_reporting(&id, &|received, declared| {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
transfer.received.store(received, Relaxed);
|
||||
transfer.declared.store(declared.unwrap_or(0), Relaxed);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Store before returning, so the bytes are on disk by the time the
|
||||
// image is on screen. Doing it after would leave a window where
|
||||
@@ -936,6 +977,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Whoever is watching a path reads the holder's progress through the
|
||||
/// registry, and loses it when the transfer ends — a step onto a frame
|
||||
/// the prefetcher is fetching shows that transfer's bar.
|
||||
#[test]
|
||||
fn progress_is_readable_by_path_while_claimed() {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
let registry = InFlight::default();
|
||||
assert_eq!(registry.progress("a.CR2"), None);
|
||||
|
||||
let claim = registry.claim("a.CR2").unwrap();
|
||||
assert_eq!(registry.progress("a.CR2"), Some((0, None)));
|
||||
|
||||
claim.transfer.received.store(1024, Relaxed);
|
||||
claim.transfer.declared.store(4096, Relaxed);
|
||||
assert_eq!(registry.progress("a.CR2"), Some((1024, Some(4096))));
|
||||
assert_eq!(registry.progress("b.CR2"), None);
|
||||
|
||||
drop(claim);
|
||||
assert_eq!(registry.progress("a.CR2"), None);
|
||||
}
|
||||
|
||||
/// Different photographs never wait on each other.
|
||||
#[test]
|
||||
fn distinct_paths_are_claimed_independently() {
|
||||
|
||||
Reference in New Issue
Block a user