perf: eliminate double disk read in thumbnail generation (read-once buffer)
This commit is contained in:
@@ -192,7 +192,11 @@ impl ThumbnailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Generate thumbnail (CPU-bound, runs in spawn_blocking)
|
// 2. Generate thumbnail (CPU-bound, runs in spawn_blocking)
|
||||||
tracing::info!("🎨 Generating thumbnail: {} {:?}", file_id_owned, size);
|
tracing::info!(
|
||||||
|
"🎨 Generating thumbnail: {} {:?}",
|
||||||
|
file_id_owned,
|
||||||
|
size
|
||||||
|
);
|
||||||
match self.generate_thumbnail(&original_owned, size).await {
|
match self.generate_thumbnail(&original_owned, size).await {
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
// Save to disk (best-effort — don't fail the request)
|
// Save to disk (best-effort — don't fail the request)
|
||||||
@@ -241,16 +245,18 @@ impl ThumbnailService {
|
|||||||
let max_dim = size.max_dimension();
|
let max_dim = size.max_dimension();
|
||||||
|
|
||||||
// Acquire semaphore permit — bounds peak RAM from concurrent decodes
|
// Acquire semaphore permit — bounds peak RAM from concurrent decodes
|
||||||
let _permit = self
|
let _permit = self.decode_semaphore.acquire().await
|
||||||
.decode_semaphore
|
|
||||||
.acquire()
|
|
||||||
.await
|
|
||||||
.map_err(|_| ThumbnailError::TaskError("Decode semaphore closed".into()))?;
|
.map_err(|_| ThumbnailError::TaskError("Decode semaphore closed".into()))?;
|
||||||
|
|
||||||
// Run image processing in blocking thread pool
|
// Run image processing in blocking thread pool
|
||||||
let result = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, ThumbnailError> {
|
let result = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, ThumbnailError> {
|
||||||
// Safety check: read dimensions from headers only (no full decode)
|
// Single read: load file once into memory, then work from the buffer
|
||||||
let (w, h) = image::ImageReader::open(&path)
|
let data = std::fs::read(&path)
|
||||||
|
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||||
|
|
||||||
|
// Safety check: read dimensions from in-memory buffer (no 2nd I/O)
|
||||||
|
let (w, h) = image::ImageReader::new(std::io::Cursor::new(&data))
|
||||||
|
.with_guessed_format()
|
||||||
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?
|
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?
|
||||||
.into_dimensions()
|
.into_dimensions()
|
||||||
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||||
@@ -261,8 +267,9 @@ impl ThumbnailService {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load image (full decode — now safe within semaphore + resolution guard)
|
// Full decode from the same in-memory buffer (no 2nd disk read)
|
||||||
let img = image::open(&path).map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
let img = image::load_from_memory(&data)
|
||||||
|
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||||
|
|
||||||
// Calculate new dimensions preserving aspect ratio
|
// Calculate new dimensions preserving aspect ratio
|
||||||
let (orig_width, orig_height) = (img.width(), img.height());
|
let (orig_width, orig_height) = (img.width(), img.height());
|
||||||
@@ -277,9 +284,9 @@ impl ThumbnailService {
|
|||||||
// Adaptive filter: faster filters for smaller sizes where
|
// Adaptive filter: faster filters for smaller sizes where
|
||||||
// quality difference vs Lanczos3 is imperceptible
|
// quality difference vs Lanczos3 is imperceptible
|
||||||
let filter = match size {
|
let filter = match size {
|
||||||
ThumbnailSize::Icon => FilterType::Triangle, // 150px — max speed
|
ThumbnailSize::Icon => FilterType::Triangle, // 150px — max speed
|
||||||
ThumbnailSize::Preview => FilterType::CatmullRom, // 400px — good balance
|
ThumbnailSize::Preview => FilterType::CatmullRom, // 400px — good balance
|
||||||
ThumbnailSize::Large => FilterType::CatmullRom, // 800px — sufficient quality
|
ThumbnailSize::Large => FilterType::CatmullRom, // 800px — sufficient quality
|
||||||
};
|
};
|
||||||
let thumbnail = img.resize(new_width, new_height, filter);
|
let thumbnail = img.resize(new_width, new_height, filter);
|
||||||
|
|
||||||
@@ -311,10 +318,7 @@ impl ThumbnailService {
|
|||||||
let _permit = match self.decode_semaphore.acquire().await {
|
let _permit = match self.decode_semaphore.acquire().await {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
tracing::warn!(
|
tracing::warn!("Decode semaphore closed, skipping thumbnails for {}", file_id);
|
||||||
"Decode semaphore closed, skipping thumbnails for {}",
|
|
||||||
file_id
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -323,8 +327,13 @@ impl ThumbnailService {
|
|||||||
|
|
||||||
// Single spawn_blocking: 1 read + 1 decode + 3 resize + 3 encode
|
// Single spawn_blocking: 1 read + 1 decode + 3 resize + 3 encode
|
||||||
let results = tokio::task::spawn_blocking(move || {
|
let results = tokio::task::spawn_blocking(move || {
|
||||||
// Safety check: read dimensions from headers only (no full decode)
|
// Single read: load file once into memory
|
||||||
let (w, h) = image::ImageReader::open(&path)
|
let data = std::fs::read(&path)
|
||||||
|
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||||
|
|
||||||
|
// Safety check: read dimensions from in-memory buffer (no 2nd I/O)
|
||||||
|
let (w, h) = image::ImageReader::new(std::io::Cursor::new(&data))
|
||||||
|
.with_guessed_format()
|
||||||
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?
|
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?
|
||||||
.into_dimensions()
|
.into_dimensions()
|
||||||
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||||
@@ -335,8 +344,9 @@ impl ThumbnailService {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let img =
|
// Full decode from the same in-memory buffer (no 2nd disk read)
|
||||||
image::open(&path).map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
let img = image::load_from_memory(&data)
|
||||||
|
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||||
|
|
||||||
let (orig_w, orig_h) = (img.width(), img.height());
|
let (orig_w, orig_h) = (img.width(), img.height());
|
||||||
|
|
||||||
@@ -354,15 +364,18 @@ impl ThumbnailService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let filter = match size {
|
let filter = match size {
|
||||||
ThumbnailSize::Icon => FilterType::Triangle,
|
ThumbnailSize::Icon => FilterType::Triangle,
|
||||||
ThumbnailSize::Preview => FilterType::CatmullRom,
|
ThumbnailSize::Preview => FilterType::CatmullRom,
|
||||||
ThumbnailSize::Large => FilterType::CatmullRom,
|
ThumbnailSize::Large => FilterType::CatmullRom,
|
||||||
};
|
};
|
||||||
let thumb = img.resize(new_w, new_h, filter);
|
let thumb = img.resize(new_w, new_h, filter);
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
thumb
|
thumb
|
||||||
.write_to(&mut std::io::Cursor::new(&mut buf), ImageFormat::WebP)
|
.write_to(
|
||||||
|
&mut std::io::Cursor::new(&mut buf),
|
||||||
|
ImageFormat::WebP,
|
||||||
|
)
|
||||||
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||||
|
|
||||||
Ok((size, Bytes::from(buf)))
|
Ok((size, Bytes::from(buf)))
|
||||||
@@ -375,11 +388,15 @@ impl ThumbnailService {
|
|||||||
let thumbnails = match results {
|
let thumbnails = match results {
|
||||||
Ok(Ok(t)) => t,
|
Ok(Ok(t)) => t,
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
tracing::warn!("Thumbnail generation failed for {}: {}", file_id, e);
|
tracing::warn!(
|
||||||
|
"Thumbnail generation failed for {}: {}", file_id, e
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Thumbnail task panicked for {}: {}", file_id, e);
|
tracing::warn!(
|
||||||
|
"Thumbnail task panicked for {}: {}", file_id, e
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user