From 9794bba975b143daf53e265246e146ac5405e9b8 Mon Sep 17 00:00:00 2001 From: BillionClaw <267901332+BillionClaw@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:51:39 +0800 Subject: [PATCH] fix(thumbnails): drop encoded image data after decoding to reduce memory usage Explicitly drop the encoded image buffer after decoding and extracting EXIF orientation data. This reduces peak memory consumption during thumbnail generation by the size of the original file. The encoded data is no longer needed once the image is decoded into a DynamicImage, but it was being held in memory until the end of the spawn_blocking scope. Fixes excessive memory consumption in thumbnail generation. --- src/infrastructure/services/thumbnail_service.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/infrastructure/services/thumbnail_service.rs b/src/infrastructure/services/thumbnail_service.rs index fa696295..7febf397 100755 --- a/src/infrastructure/services/thumbnail_service.rs +++ b/src/infrastructure/services/thumbnail_service.rs @@ -347,8 +347,10 @@ impl ThumbnailService { /// Generate a thumbnail from an image file. /// /// Concurrency is bounded by `decode_semaphore` to prevent OOM when - /// many images are uploaded simultaneously. Resolution is also + /// many images are uploaded simultaneously. Resolution is also /// capped at `MAX_DECODE_PIXELS` to reject pathologically large images. + /// After decoding, the encoded image buffer is explicitly dropped before + /// processing to minimize peak memory usage. async fn generate_thumbnail( &self, original_path: &Path, @@ -395,6 +397,8 @@ impl ThumbnailService { let orientation = ExifService::extract(&data) .and_then(|m| m.orientation) .unwrap_or(1); + // Free the encoded image data now that image is decoded and EXIF extracted + drop(data); apply_orientation(img, orientation) }; @@ -436,9 +440,11 @@ impl ThumbnailService { /// Generate all thumbnail sizes for a file in the background. /// /// Loads the image **once** and produces all 3 sizes (Icon, Preview, - /// Large) inside a single `spawn_blocking` call. This avoids 3× + /// Large) inside a single `spawn_blocking` call. This avoids 3× /// I/O reads and 3× JPEG/PNG decode — reducing CPU time by ~45% /// and peak RAM from ~540 MB to ~180 MB for concurrent uploads. + /// The encoded image buffer is explicitly dropped after decoding + /// to further reduce peak memory by the size of the original file. pub fn generate_all_sizes_background(self: Arc, file_id: String, original_path: PathBuf) { tokio::spawn(async move { tracing::info!("🖼️ Background thumbnail generation starting: {}", file_id); @@ -488,6 +494,8 @@ impl ThumbnailService { let orientation = ExifService::extract(&data) .and_then(|m| m.orientation) .unwrap_or(1); + // Free the encoded image data now that image is decoded and EXIF extracted + drop(data); apply_orientation(img, orientation) };