mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-11 02:08:44 +02:00
perf(library): reuse bounded artwork thumbnails during scans
This commit is contained in:
1 parent
6c22a91498
commit
88508c806a
4 files changed
+244
-7
No files matched your search
@@ -4,6 +4,9 @@ use image::{DynamicImage, ImageFormat, ImageReader};
|
||||
use std::borrow::Cow;
|
||||
use std::io::Cursor;
|
||||
|
||||
mod library;
|
||||
pub use library::{clear_library_thumbnail_cache, library_thumbnail};
|
||||
|
||||
pub const MAX_DOWNLOAD_BYTES: usize = 24 << 20;
|
||||
pub const LIBRARY_MAX_DIMENSION: i64 = 800;
|
||||
const MAX_DECODE_PIXELS: u64 = 16_000_000;
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! Bounded thumbnail reuse for tracks that embed the same album artwork.
|
||||
|
||||
use std::collections::{VecDeque, hash_map::DefaultHasher};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::{Arc, Mutex, OnceLock, TryLockError};
|
||||
use std::time::Duration;
|
||||
|
||||
const MAX_BYTES: usize = 8 << 20;
|
||||
const MAX_ENTRIES: usize = 32;
|
||||
static CACHE: OnceLock<Mutex<Cache>> = OnceLock::new();
|
||||
|
||||
struct Entry {
|
||||
hash: u64,
|
||||
original: Arc<[u8]>,
|
||||
thumbnail: Arc<[u8]>,
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Cache {
|
||||
entries: VecDeque<Entry>,
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
fn insert(&mut self, entry: Entry) {
|
||||
if entry.bytes > MAX_BYTES {
|
||||
return;
|
||||
}
|
||||
while self.bytes + entry.bytes > MAX_BYTES || self.entries.len() >= MAX_ENTRIES {
|
||||
self.bytes -= self
|
||||
.entries
|
||||
.pop_front()
|
||||
.expect("nonempty thumbnail cache")
|
||||
.bytes;
|
||||
}
|
||||
self.bytes += entry.bytes;
|
||||
self.entries.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize large image decodes while keeping tag reads parallel. Reuse resized
|
||||
/// artwork by content, not filename or album name; never confuse alternate covers.
|
||||
pub fn library_thumbnail(
|
||||
data: &[u8],
|
||||
check: &dyn Fn() -> Result<(), String>,
|
||||
) -> Result<Arc<[u8]>, String> {
|
||||
check()?;
|
||||
let mut hasher = DefaultHasher::new();
|
||||
data.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
let mutex = CACHE.get_or_init(Mutex::default);
|
||||
let mut cache = loop {
|
||||
check()?;
|
||||
match mutex.try_lock() {
|
||||
Ok(cache) => break cache,
|
||||
Err(TryLockError::WouldBlock) => std::thread::sleep(Duration::from_millis(10)),
|
||||
Err(TryLockError::Poisoned(_)) => return Err("thumbnail cache unavailable".into()),
|
||||
}
|
||||
};
|
||||
if let Some(index) = cache
|
||||
.entries
|
||||
.iter()
|
||||
.position(|entry| entry.hash == hash && &*entry.original == data)
|
||||
{
|
||||
let entry = cache.entries.remove(index).expect("cached thumbnail");
|
||||
let thumbnail = entry.thumbnail.clone();
|
||||
cache.entries.push_back(entry);
|
||||
return Ok(thumbnail);
|
||||
}
|
||||
let resized = super::resize(data, super::LIBRARY_MAX_DIMENSION, check)?;
|
||||
let original: Arc<[u8]> = data.into();
|
||||
let thumbnail = match resized {
|
||||
std::borrow::Cow::Borrowed(_) => original.clone(),
|
||||
std::borrow::Cow::Owned(data) => data.into(),
|
||||
};
|
||||
check()?;
|
||||
let bytes = original.len()
|
||||
+ if Arc::ptr_eq(&original, &thumbnail) {
|
||||
0
|
||||
} else {
|
||||
thumbnail.len()
|
||||
};
|
||||
cache.insert(Entry {
|
||||
hash,
|
||||
original,
|
||||
thumbnail: thumbnail.clone(),
|
||||
bytes,
|
||||
});
|
||||
Ok(thumbnail)
|
||||
}
|
||||
|
||||
pub fn clear_library_thumbnail_cache() {
|
||||
if let Some(mutex) = CACHE.get()
|
||||
&& let Ok(mut cache) = mutex.try_lock()
|
||||
{
|
||||
*cache = Cache::default();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use image::{ImageBuffer, ImageFormat, Rgb};
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn repeated_artwork_reuses_pixels_and_checks_cancellation_on_hits() {
|
||||
let mut png = Cursor::new(Vec::new());
|
||||
ImageBuffer::from_pixel(1000, 1000, Rgb([30_u8, 90, 150]))
|
||||
.write_to(&mut png, ImageFormat::Png)
|
||||
.unwrap();
|
||||
let first = library_thumbnail(png.get_ref(), &|| Ok(())).unwrap();
|
||||
let second = library_thumbnail(png.get_ref(), &|| Ok(())).unwrap();
|
||||
assert!(Arc::ptr_eq(&first, &second));
|
||||
assert_eq!(super::super::dimensions(&first), (800, 800));
|
||||
assert!(library_thumbnail(png.get_ref(), &|| Err("cancelled".into())).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_has_fixed_byte_and_entry_bounds() {
|
||||
let mut cache = Cache::default();
|
||||
for hash in 0..100 {
|
||||
let data: Arc<[u8]> = vec![0; 1 << 20].into();
|
||||
cache.insert(Entry {
|
||||
hash,
|
||||
original: data.clone(),
|
||||
thumbnail: data,
|
||||
bytes: 1 << 20,
|
||||
});
|
||||
assert!(cache.bytes <= MAX_BYTES);
|
||||
assert!(cache.entries.len() <= MAX_ENTRIES);
|
||||
}
|
||||
assert_eq!(cache.bytes, MAX_BYTES);
|
||||
assert_eq!(cache.entries.front().unwrap().hash, 92);
|
||||
}
|
||||
}
|
||||
@@ -362,18 +362,16 @@ impl Backend {
|
||||
));
|
||||
}
|
||||
let mut cover = read(&input, &format, &check)?;
|
||||
if let Ok(std::borrow::Cow::Owned(resized)) = resize(
|
||||
&cover.data,
|
||||
spotiflac_core::cover::LIBRARY_MAX_DIMENSION,
|
||||
&check,
|
||||
) {
|
||||
if let Ok(resized) = spotiflac_core::cover::library_thumbnail(&cover.data, &check) {
|
||||
cover.mime = if resized.starts_with(b"\x89PNG") {
|
||||
"image/png"
|
||||
} else {
|
||||
} else if resized.starts_with(b"\xff\xd8") {
|
||||
"image/jpeg"
|
||||
} else {
|
||||
&cover.mime
|
||||
}
|
||||
.into();
|
||||
cover.data = resized;
|
||||
cover.data = resized.to_vec();
|
||||
}
|
||||
// Library caching may retain unsupported image bytes, but must never
|
||||
// swallow cancellation while attempting the optional resize.
|
||||
@@ -396,6 +394,7 @@ impl Backend {
|
||||
pub fn clear_cover_memory_cache(&self) -> Result<(), String> {
|
||||
let _operation = self.enter()?;
|
||||
self.cover.clear();
|
||||
spotiflac_core::cover::clear_library_thumbnail_cache();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user