improve and simplify heuristics
This commit is contained in:
parent
006b69d98b
commit
76c56d5d3b
2 changed files with 277 additions and 362 deletions
|
|
@ -8,10 +8,6 @@ use std::{
|
||||||
/// Bytes per pixel in RGBA format (Red, Green, Blue, Alpha = 4 bytes)
|
/// Bytes per pixel in RGBA format (Red, Green, Blue, Alpha = 4 bytes)
|
||||||
pub const RGBA_BYTES_PER_PIXEL: u64 = 4;
|
pub const RGBA_BYTES_PER_PIXEL: u64 = 4;
|
||||||
|
|
||||||
/// Overhead factor for image decoding operations (30% additional memory for decode buffers,
|
|
||||||
/// fragment allocations, and intermediate representations during image decoding)
|
|
||||||
const DECODE_OVERHEAD_FACTOR: f64 = 1.3;
|
|
||||||
|
|
||||||
/// System memory reserve in MB to maintain for system stability (prevents thrashing)
|
/// System memory reserve in MB to maintain for system stability (prevents thrashing)
|
||||||
/// Note: RAM checking is currently only available on Linux via procfs.
|
/// Note: RAM checking is currently only available on Linux via procfs.
|
||||||
/// On Windows and macOS, only GPU buffer limits are enforced.
|
/// On Windows and macOS, only GPU buffer limits are enforced.
|
||||||
|
|
@ -27,11 +23,6 @@ const GALLERY_MEMORY_LIMIT_MB: u64 = 2000;
|
||||||
/// Must match the atlas SIZE constant in libcosmic/iced/wgpu/src/image/atlas.rs
|
/// Must match the atlas SIZE constant in libcosmic/iced/wgpu/src/image/atlas.rs
|
||||||
pub const ATLAS_FRAGMENT_SIZE: u32 = 4096;
|
pub const ATLAS_FRAGMENT_SIZE: u32 = 4096;
|
||||||
|
|
||||||
/// Conservative GPU buffer size limit in MB. Each atlas fragment can be up to this size.
|
|
||||||
/// Based on wgpu device limits - most GPUs support at least 256MB buffers.
|
|
||||||
/// Reference: https://docs.rs/wgpu/latest/wgpu/struct.Limits.html#structfield.max_buffer_size
|
|
||||||
const MAX_GPU_BUFFER_MB: u64 = 256;
|
|
||||||
|
|
||||||
/// Conversion factor: 1 MB = 1024 * 1024 bytes (binary megabyte, used for RAM calculations)
|
/// Conversion factor: 1 MB = 1024 * 1024 bytes (binary megabyte, used for RAM calculations)
|
||||||
pub const MB_TO_BYTES: u64 = 1024 * 1024;
|
pub const MB_TO_BYTES: u64 = 1024 * 1024;
|
||||||
|
|
||||||
|
|
@ -39,8 +30,78 @@ pub const MB_TO_BYTES: u64 = 1024 * 1024;
|
||||||
/// The image crate's memory limits use decimal MB, not binary MB.
|
/// The image crate's memory limits use decimal MB, not binary MB.
|
||||||
pub const DECIMAL_MB_TO_BYTES: u64 = 1000 * 1000;
|
pub const DECIMAL_MB_TO_BYTES: u64 = 1000 * 1000;
|
||||||
|
|
||||||
/// Maximum dimension for image decoding
|
/// Check if an image's dimensions would exceed the available memory budget.
|
||||||
pub const MAX_DIMENSION_FOR_DECODE: u32 = 65536;
|
/// Returns true if the image is too large to decode.
|
||||||
|
pub fn exceeds_memory_limit(width: u32, height: u32, memory_limit_mb: u64) -> bool {
|
||||||
|
let Some(bytes_needed) = calculate_image_memory(width, height) else {
|
||||||
|
// Overflow in calculation means it definitely exceeds any reasonable limit
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_bytes = memory_limit_mb * MB_TO_BYTES;
|
||||||
|
bytes_needed > max_bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an image should use GPU tiling for display.
|
||||||
|
/// Images larger than the atlas fragment size need to be split into tiles for GPU upload.
|
||||||
|
pub fn should_use_tiling(width: u32, height: u32) -> bool {
|
||||||
|
width > ATLAS_FRAGMENT_SIZE || height > ATLAS_FRAGMENT_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine if an image should use the dedicated worker for thumbnail generation.
|
||||||
|
/// Returns (use_dedicated_worker, effective_max_mb, effective_jobs).
|
||||||
|
///
|
||||||
|
/// Large images that exceed per-worker memory budget get routed to a dedicated worker
|
||||||
|
/// with full memory budget. Smaller images use the normal parallel worker pool.
|
||||||
|
pub fn should_use_dedicated_worker(
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
total_budget_mb: u64,
|
||||||
|
parallel_workers: usize,
|
||||||
|
) -> (bool, u64, usize) {
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
log::warn!(
|
||||||
|
"Invalid image dimensions {}x{}, using normal queue",
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
);
|
||||||
|
return (false, total_budget_mb, parallel_workers);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(bytes_needed) = calculate_image_memory(width, height) else {
|
||||||
|
log::warn!(
|
||||||
|
"Image dimensions {}x{} overflow memory calculation, using normal queue",
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
);
|
||||||
|
return (false, total_budget_mb, parallel_workers);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mb_needed = bytes_needed / MB_TO_BYTES;
|
||||||
|
let per_worker_budget_mb = total_budget_mb / parallel_workers as u64;
|
||||||
|
|
||||||
|
if mb_needed > per_worker_budget_mb {
|
||||||
|
log::info!(
|
||||||
|
"Large image {}x{} needs {}MB (exceeds per-worker {}MB), using dedicated worker",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
mb_needed,
|
||||||
|
per_worker_budget_mb
|
||||||
|
);
|
||||||
|
// Use dedicated worker with full budget
|
||||||
|
(true, total_budget_mb, 1)
|
||||||
|
} else {
|
||||||
|
log::debug!(
|
||||||
|
"Normal image {}x{} needs {}MB (within per-worker {}MB), using parallel workers",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
mb_needed,
|
||||||
|
per_worker_budget_mb
|
||||||
|
);
|
||||||
|
// Use parallel worker pool with shared budget
|
||||||
|
(false, total_budget_mb, parallel_workers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the dimensions of an image without fully decoding it
|
/// Get the dimensions of an image without fully decoding it
|
||||||
pub fn get_image_dimensions(path: &Path) -> Option<(u32, u32)> {
|
pub fn get_image_dimensions(path: &Path) -> Option<(u32, u32)> {
|
||||||
|
|
@ -67,17 +128,67 @@ pub fn get_image_dimensions(path: &Path) -> Option<(u32, u32)> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if there's sufficient memory to decode an image.
|
/// Calculate the memory required to decode an image in bytes.
|
||||||
///
|
/// Returns None if the calculation overflows.
|
||||||
/// This function performs two types of checks:
|
fn calculate_image_memory(width: u32, height: u32) -> Option<u64> {
|
||||||
/// 1. System RAM availability (Linux only via procfs)
|
let pixels = (width as u64).checked_mul(height as u64)?;
|
||||||
/// 2. GPU buffer limits (all platforms)
|
pixels.checked_mul(RGBA_BYTES_PER_PIXEL)
|
||||||
///
|
}
|
||||||
/// Platform-specific behavior:
|
|
||||||
/// - Linux: Full RAM checking via /proc/meminfo + GPU checks
|
/// Check if there's sufficient system RAM to decode an image (Linux only).
|
||||||
/// - Windows/macOS: GPU buffer checks only (RAM checking not yet implemented)
|
|
||||||
///
|
|
||||||
/// Returns: (has_memory, error_message)
|
/// Returns: (has_memory, error_message)
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn check_ram_available(width: u32, height: u32) -> (bool, Option<String>) {
|
||||||
|
use procfs::Current;
|
||||||
|
|
||||||
|
let Some(bytes_needed) = calculate_image_memory(width, height) else {
|
||||||
|
let error_msg = format!(
|
||||||
|
"Image dimensions too large: {}x{} causes overflow in memory calculation",
|
||||||
|
width, height
|
||||||
|
);
|
||||||
|
log::error!("{}", error_msg);
|
||||||
|
return (false, Some(error_msg));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mb_needed = bytes_needed / MB_TO_BYTES;
|
||||||
|
|
||||||
|
match procfs::Meminfo::current() {
|
||||||
|
Ok(meminfo) => {
|
||||||
|
// MemAvailable includes reclaimable cache and is the best estimate of
|
||||||
|
// actually available memory for new allocations
|
||||||
|
let available_kb = meminfo.mem_available.unwrap_or(0);
|
||||||
|
let available_bytes = available_kb * 1024;
|
||||||
|
|
||||||
|
// Maintain system reserve to prevent thrashing and OOM killer
|
||||||
|
let min_reserve_bytes = SYSTEM_MEMORY_RESERVE_MB * MB_TO_BYTES;
|
||||||
|
let usable_bytes = available_bytes.saturating_sub(min_reserve_bytes);
|
||||||
|
|
||||||
|
if bytes_needed > usable_bytes {
|
||||||
|
let available_mb = available_bytes / MB_TO_BYTES;
|
||||||
|
let error_msg = format!(
|
||||||
|
"Insufficient memory: need {}MB, available {}MB. Try closing other applications.",
|
||||||
|
mb_needed, available_mb
|
||||||
|
);
|
||||||
|
log::warn!("{}", error_msg);
|
||||||
|
return (false, Some(error_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
(true, None)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to read /proc/meminfo: {}. Skipping RAM check.", e);
|
||||||
|
// Graceful fallback: assume RAM is available
|
||||||
|
(true, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn check_ram_available(_width: u32, _height: u32) -> (bool, Option<String>) {
|
||||||
|
// RAM checking not implemented for this platform
|
||||||
|
(true, None)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_memory_available(width: u32, height: u32) -> (bool, Option<String>) {
|
pub fn check_memory_available(width: u32, height: u32) -> (bool, Option<String>) {
|
||||||
if width == 0 || height == 0 {
|
if width == 0 || height == 0 {
|
||||||
let error_msg = format!(
|
let error_msg = format!(
|
||||||
|
|
@ -88,113 +199,8 @@ pub fn check_memory_available(width: u32, height: u32) -> (bool, Option<String>)
|
||||||
return (false, Some(error_msg));
|
return (false, Some(error_msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
let pixels = match (width as u64).checked_mul(height as u64) {
|
// Check system RAM availability
|
||||||
Some(p) => p,
|
check_ram_available(width, height)
|
||||||
None => {
|
|
||||||
let error_msg = format!(
|
|
||||||
"Image dimensions too large: {}x{} causes overflow in pixel calculation",
|
|
||||||
width, height
|
|
||||||
);
|
|
||||||
log::error!("{}", error_msg);
|
|
||||||
return (false, Some(error_msg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let bytes_needed = match pixels.checked_mul(RGBA_BYTES_PER_PIXEL) {
|
|
||||||
Some(b) => b,
|
|
||||||
None => {
|
|
||||||
let error_msg = format!(
|
|
||||||
"Image memory requirements overflow: {}x{} pixels requires more than {} bytes",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
u64::MAX
|
|
||||||
);
|
|
||||||
log::error!("{}", error_msg);
|
|
||||||
return (false, Some(error_msg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add overhead for decode buffers, fragment allocations, and intermediate representations
|
|
||||||
let bytes_with_overhead = (bytes_needed as f64 * DECODE_OVERHEAD_FACTOR) as u64;
|
|
||||||
let mb_needed = bytes_with_overhead / MB_TO_BYTES;
|
|
||||||
|
|
||||||
// Check system RAM availability (Linux only)
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
use procfs::Current;
|
|
||||||
match procfs::Meminfo::current() {
|
|
||||||
Ok(meminfo) => {
|
|
||||||
// MemAvailable includes reclaimable cache and is the best estimate of
|
|
||||||
// actually available memory for new allocations
|
|
||||||
let available_kb = meminfo.mem_available.unwrap_or(0);
|
|
||||||
let available_bytes = available_kb * 1024;
|
|
||||||
|
|
||||||
// Maintain system reserve to prevent thrashing and OOM killer
|
|
||||||
let min_reserve_bytes = SYSTEM_MEMORY_RESERVE_MB * MB_TO_BYTES;
|
|
||||||
let usable_bytes = available_bytes.saturating_sub(min_reserve_bytes);
|
|
||||||
|
|
||||||
if bytes_with_overhead > usable_bytes {
|
|
||||||
let available_mb = available_bytes / MB_TO_BYTES;
|
|
||||||
let error_msg = format!(
|
|
||||||
"Insufficient memory: need {}MB, available {}MB. Try closing other applications.",
|
|
||||||
mb_needed, available_mb
|
|
||||||
);
|
|
||||||
log::warn!("{}", error_msg);
|
|
||||||
return (false, Some(error_msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to read /proc/meminfo: {}. Skipping RAM check.", e);
|
|
||||||
// Graceful fallback: continue to GPU checks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: RAM checking not implemented for Windows/macOS
|
|
||||||
// These platforms will only validate against GPU buffer limits below
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
{
|
|
||||||
log::debug!(
|
|
||||||
"RAM checking not available on this platform. Only GPU limits will be enforced."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check GPU fragment/atlas tile limits
|
|
||||||
// Large images are split into atlas fragments for GPU upload.
|
|
||||||
// Each fragment must fit within GPU buffer size limits.
|
|
||||||
let fragment_bytes =
|
|
||||||
(ATLAS_FRAGMENT_SIZE as u64) * (ATLAS_FRAGMENT_SIZE as u64) * RGBA_BYTES_PER_PIXEL;
|
|
||||||
let max_gpu_buffer_bytes = MAX_GPU_BUFFER_MB * MB_TO_BYTES;
|
|
||||||
|
|
||||||
let fragments_x = (width + ATLAS_FRAGMENT_SIZE - 1) / ATLAS_FRAGMENT_SIZE;
|
|
||||||
let fragments_y = (height + ATLAS_FRAGMENT_SIZE - 1) / ATLAS_FRAGMENT_SIZE;
|
|
||||||
let fragment_count = fragments_x as u64 * fragments_y as u64;
|
|
||||||
|
|
||||||
// Fragments are uploaded sequentially, so we only need one fragment buffer at a time.
|
|
||||||
// However, each individual fragment must fit within GPU buffer size limits.
|
|
||||||
if fragment_bytes > max_gpu_buffer_bytes {
|
|
||||||
let max_dimension = (MAX_GPU_BUFFER_MB * MB_TO_BYTES / RGBA_BYTES_PER_PIXEL) as f64;
|
|
||||||
let max_dimension = (max_dimension.sqrt() as u32).saturating_sub(100); // Add safety margin
|
|
||||||
|
|
||||||
let error_msg = format!(
|
|
||||||
"Image too large for GPU: {}x{} pixels exceeds GPU buffer limits. \
|
|
||||||
Maximum supported dimension is approximately {}x{} pixels.",
|
|
||||||
width, height, max_dimension, max_dimension
|
|
||||||
);
|
|
||||||
log::error!("{}", error_msg);
|
|
||||||
return (false, Some(error_msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!(
|
|
||||||
"Memory check passed: {}x{} image needs {}MB RAM, will use {} GPU fragment(s) of {}MB each",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
mb_needed,
|
|
||||||
fragment_count,
|
|
||||||
fragment_bytes / MB_TO_BYTES
|
|
||||||
);
|
|
||||||
|
|
||||||
(true, None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode a large image asynchronously in a blocking thread pool.
|
/// Decode a large image asynchronously in a blocking thread pool.
|
||||||
|
|
@ -256,7 +262,6 @@ pub async fn decode_large_image(path: PathBuf) -> Option<(PathBuf, u32, u32, Vec
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Manages state and operations for large image decoding in gallery mode
|
/// Manages state and operations for large image decoding in gallery mode
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct LargeImageManager {
|
pub struct LargeImageManager {
|
||||||
|
|
@ -285,17 +290,14 @@ impl LargeImageManager {
|
||||||
self.decode_errors.get(path)
|
self.decode_errors.get(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_decoding(&mut self, path: PathBuf) {
|
|
||||||
self.decoding_images.insert(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn store_decoded(&mut self, path: PathBuf, handle: widget::image::Handle) {
|
pub fn store_decoded(&mut self, path: PathBuf, handle: widget::image::Handle) {
|
||||||
self.decoded_images.insert(path.clone(), handle);
|
self.decoded_images.insert(path.clone(), handle);
|
||||||
self.decoding_images.remove(&path);
|
self.decoding_images.remove(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store_error(&mut self, path: PathBuf, error: String) {
|
pub fn store_error(&mut self, path: PathBuf, error: String) {
|
||||||
self.decode_errors.insert(path, error);
|
self.decode_errors.insert(path.clone(), error);
|
||||||
|
self.decoding_images.remove(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_error(&mut self, path: &Path) {
|
pub fn clear_error(&mut self, path: &Path) {
|
||||||
|
|
@ -317,4 +319,71 @@ impl LargeImageManager {
|
||||||
pub fn cache_is_empty(&self) -> bool {
|
pub fn cache_is_empty(&self) -> bool {
|
||||||
self.decoded_images.is_empty()
|
self.decoded_images.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempt to decode a large image, checking memory availability first.
|
||||||
|
/// Returns true if decode was initiated, false if skipped due to insufficient memory.
|
||||||
|
pub fn try_decode(&mut self, path: &PathBuf) -> bool {
|
||||||
|
self.clear_error(path);
|
||||||
|
|
||||||
|
// Check if already decoded or decoding
|
||||||
|
if self.get_decoded(path).is_some() || self.is_decoding(path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((width, height)) = get_image_dimensions(path) else {
|
||||||
|
self.store_error(path.clone(), "Failed to read image dimensions".to_string());
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !self.ensure_memory_available(path, width, height) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as decoding
|
||||||
|
self.decoding_images.insert(path.clone());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if sufficient memory is available, clearing cache if needed.
|
||||||
|
/// Returns true if memory is available, false otherwise.
|
||||||
|
fn ensure_memory_available(&mut self, path: &PathBuf, width: u32, height: u32) -> bool {
|
||||||
|
let (has_memory, error_opt) = check_memory_available(width, height);
|
||||||
|
|
||||||
|
if has_memory {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.cache_is_empty() {
|
||||||
|
if let Some(error_msg) = error_opt {
|
||||||
|
self.store_error(path.clone(), error_msg);
|
||||||
|
log::warn!(
|
||||||
|
"Cannot load {}: insufficient memory and cache is empty",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Insufficient memory, clearing {} cached images",
|
||||||
|
self.cache_size()
|
||||||
|
);
|
||||||
|
self.clear_cache();
|
||||||
|
|
||||||
|
let (has_memory_after_clear, error_opt_after) = check_memory_available(width, height);
|
||||||
|
|
||||||
|
if has_memory_after_clear {
|
||||||
|
log::info!("Memory available after cache clear, proceeding with decode");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(error_msg) = error_opt_after {
|
||||||
|
self.store_error(path.clone(), error_msg);
|
||||||
|
log::warn!(
|
||||||
|
"Cannot load {}: insufficient memory even after cache clear",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
302
src/tab.rs
302
src/tab.rs
|
|
@ -80,9 +80,8 @@ use crate::{
|
||||||
dialog::DialogKind,
|
dialog::DialogKind,
|
||||||
fl,
|
fl,
|
||||||
large_image::{
|
large_image::{
|
||||||
DECIMAL_MB_TO_BYTES, LargeImageManager, MAX_DIMENSION_FOR_DECODE, ATLAS_FRAGMENT_SIZE,
|
LargeImageManager, decode_large_image, exceeds_memory_limit, should_use_dedicated_worker,
|
||||||
MB_TO_BYTES, RGBA_BYTES_PER_PIXEL, check_memory_available, decode_large_image,
|
should_use_tiling,
|
||||||
get_image_dimensions,
|
|
||||||
},
|
},
|
||||||
localize::{LANGUAGE_SORTER, LOCALE},
|
localize::{LANGUAGE_SORTER, LOCALE},
|
||||||
menu, mime_app,
|
menu, mime_app,
|
||||||
|
|
@ -103,14 +102,11 @@ const MAX_SEARCH_RESULTS: usize = 200;
|
||||||
//TODO: configurable thumbnail size?
|
//TODO: configurable thumbnail size?
|
||||||
const THUMBNAIL_SIZE: u32 = (ICON_SIZE_GRID as u32) * (ICON_SCALE_MAX as u32);
|
const THUMBNAIL_SIZE: u32 = (ICON_SIZE_GRID as u32) * (ICON_SCALE_MAX as u32);
|
||||||
|
|
||||||
// Semaphore for normal-sized images (4 parallel workers)
|
// Thumbnail generation semaphore - limits parallel thumbnail workers
|
||||||
pub static THUMB_SEMAPHORE_NORMAL: LazyLock<tokio::sync::Semaphore> =
|
// Uses 4 workers for balanced throughput and memory usage
|
||||||
|
pub static THUMB_SEMAPHORE: LazyLock<tokio::sync::Semaphore> =
|
||||||
LazyLock::new(|| tokio::sync::Semaphore::const_new(4));
|
LazyLock::new(|| tokio::sync::Semaphore::const_new(4));
|
||||||
|
|
||||||
// Semaphore for large images that would exceed per-worker memory limit (1 worker with full budget)
|
|
||||||
pub static THUMB_SEMAPHORE_LARGE: LazyLock<tokio::sync::Semaphore> =
|
|
||||||
LazyLock::new(|| tokio::sync::Semaphore::const_new(1));
|
|
||||||
|
|
||||||
pub(crate) static SORT_OPTION_FALLBACK: LazyLock<FxHashMap<String, (HeadingOptions, bool)>> =
|
pub(crate) static SORT_OPTION_FALLBACK: LazyLock<FxHashMap<String, (HeadingOptions, bool)>> =
|
||||||
LazyLock::new(|| {
|
LazyLock::new(|| {
|
||||||
FxHashMap::from_iter(dirs::download_dir().into_iter().map(|dir| {
|
FxHashMap::from_iter(dirs::download_dir().into_iter().map(|dir| {
|
||||||
|
|
@ -1722,11 +1718,7 @@ impl ItemMetadata {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ItemThumbnail {
|
pub enum ItemThumbnail {
|
||||||
NotImage,
|
NotImage,
|
||||||
Image(
|
Image(widget::image::Handle, Option<(u32, u32)>),
|
||||||
widget::image::Handle,
|
|
||||||
Option<(u32, u32)>,
|
|
||||||
Option<widget::image::Handle>,
|
|
||||||
),
|
|
||||||
Svg(widget::svg::Handle),
|
Svg(widget::svg::Handle),
|
||||||
Text(widget::text_editor::Content),
|
Text(widget::text_editor::Content),
|
||||||
}
|
}
|
||||||
|
|
@ -1735,9 +1727,7 @@ impl Clone for ItemThumbnail {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::NotImage => Self::NotImage,
|
Self::NotImage => Self::NotImage,
|
||||||
Self::Image(handle, size_opt, full_handle_opt) => {
|
Self::Image(handle, size_opt) => Self::Image(handle.clone(), *size_opt),
|
||||||
Self::Image(handle.clone(), *size_opt, full_handle_opt.clone())
|
|
||||||
}
|
|
||||||
Self::Svg(handle) => Self::Svg(handle.clone()),
|
Self::Svg(handle) => Self::Svg(handle.clone()),
|
||||||
// Content cannot be cloned simply
|
// Content cannot be cloned simply
|
||||||
Self::Text(content) => {
|
Self::Text(content) => {
|
||||||
|
|
@ -1769,16 +1759,9 @@ impl ItemThumbnail {
|
||||||
Err(_) => size.map(|s| (s.pixel_size(), s.pixel_size())),
|
Err(_) => size.map(|s| (s.pixel_size(), s.pixel_size())),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create and cache the full-size handle for large images that need GPU tiling
|
|
||||||
// Images >4096 pixels get fragmented into multiple tiles for GPU upload
|
|
||||||
let full_handle = original_dims
|
|
||||||
.filter(|(w, h)| *w > ATLAS_FRAGMENT_SIZE || *h > ATLAS_FRAGMENT_SIZE)
|
|
||||||
.map(|_| widget::image::Handle::from_path(path));
|
|
||||||
|
|
||||||
return Self::Image(
|
return Self::Image(
|
||||||
widget::image::Handle::from_path(thumbnail_path),
|
widget::image::Handle::from_path(thumbnail_path),
|
||||||
original_dims,
|
original_dims,
|
||||||
full_handle,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
CachedThumbnail::Failed => {
|
CachedThumbnail::Failed => {
|
||||||
|
|
@ -1818,22 +1801,21 @@ impl ItemThumbnail {
|
||||||
let mut tried_supported_file = false;
|
let mut tried_supported_file = false;
|
||||||
// First try built-in image thumbnailer
|
// First try built-in image thumbnailer
|
||||||
if mime.type_() == mime::IMAGE && check_size("image", max_size_mb * 1000 * 1000) {
|
if mime.type_() == mime::IMAGE && check_size("image", max_size_mb * 1000 * 1000) {
|
||||||
// Check for extremely large dimensions that would cause memory issues during decoding
|
// Check if image dimensions would exceed available memory budget
|
||||||
// The GPU tiling system can handle large images, but we still need to decode them first
|
// The GPU tiling system can handle large images, but we still need to decode them first
|
||||||
// Set a reasonable limit to prevent OOM during image decoding
|
|
||||||
let dimensions_ok = match image::image_dimensions(path) {
|
let dimensions_ok = match image::image_dimensions(path) {
|
||||||
Ok((width, height)) => {
|
Ok((width, height)) => {
|
||||||
if width > MAX_DIMENSION_FOR_DECODE || height > MAX_DIMENSION_FOR_DECODE {
|
if exceeds_memory_limit(width, height, max_mem) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"skipping thumbnail generation for {}: dimensions {}x{} exceed decode limit of {}",
|
"skipping thumbnail generation for {}: {}x{} image would exceed {}MB memory budget",
|
||||||
path.display(),
|
path.display(),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
MAX_DIMENSION_FOR_DECODE
|
max_mem
|
||||||
);
|
);
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
if width > ATLAS_FRAGMENT_SIZE || height > ATLAS_FRAGMENT_SIZE {
|
if should_use_tiling(width, height) {
|
||||||
log::info!(
|
log::info!(
|
||||||
"Large image {}x{} detected, will use GPU tiling for display",
|
"Large image {}x{} detected, will use GPU tiling for display",
|
||||||
width,
|
width,
|
||||||
|
|
@ -1912,12 +1894,6 @@ impl ItemThumbnail {
|
||||||
|
|
||||||
if let Some(dyn_img) = dyn_img {
|
if let Some(dyn_img) = dyn_img {
|
||||||
let (img_width, img_height) = (dyn_img.width(), dyn_img.height());
|
let (img_width, img_height) = (dyn_img.width(), dyn_img.height());
|
||||||
let full_handle =
|
|
||||||
if img_width > ATLAS_FRAGMENT_SIZE || img_height > ATLAS_FRAGMENT_SIZE {
|
|
||||||
Some(widget::image::Handle::from_path(path))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(cacher) = thumbnail_cacher.as_ref() {
|
if let Ok(cacher) = thumbnail_cacher.as_ref() {
|
||||||
match cacher.update_with_image(dyn_img) {
|
match cacher.update_with_image(dyn_img) {
|
||||||
|
|
@ -1925,7 +1901,6 @@ impl ItemThumbnail {
|
||||||
return Self::Image(
|
return Self::Image(
|
||||||
widget::image::Handle::from_path(thumb_path),
|
widget::image::Handle::from_path(thumb_path),
|
||||||
Some((img_width, img_height)),
|
Some((img_width, img_height)),
|
||||||
full_handle,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
@ -1944,7 +1919,6 @@ impl ItemThumbnail {
|
||||||
thumbnail.into_raw(),
|
thumbnail.into_raw(),
|
||||||
),
|
),
|
||||||
Some((img_width, img_height)),
|
Some((img_width, img_height)),
|
||||||
full_handle,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2072,7 +2046,6 @@ impl ItemThumbnail {
|
||||||
image.into_raw(),
|
image.into_raw(),
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
),
|
),
|
||||||
file,
|
file,
|
||||||
));
|
));
|
||||||
|
|
@ -2158,7 +2131,7 @@ impl Item {
|
||||||
.unwrap_or(&ItemThumbnail::NotImage)
|
.unwrap_or(&ItemThumbnail::NotImage)
|
||||||
{
|
{
|
||||||
ItemThumbnail::NotImage => icon,
|
ItemThumbnail::NotImage => icon,
|
||||||
ItemThumbnail::Image(handle, _original_dims, _full_handle_opt) => {
|
ItemThumbnail::Image(handle, _original_dims) => {
|
||||||
// Preview pane: ALWAYS show thumbnail for instant, responsive UI
|
// Preview pane: ALWAYS show thumbnail for instant, responsive UI
|
||||||
// Full resolution loading happens in gallery mode
|
// Full resolution loading happens in gallery mode
|
||||||
widget::image(handle.clone()).into()
|
widget::image(handle.clone()).into()
|
||||||
|
|
@ -2996,10 +2969,16 @@ impl Tab {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(ItemThumbnail::Image(_, _, _)) = &item.thumbnail_opt else {
|
let Some(ItemThumbnail::Image(_, original_dims)) = &item.thumbnail_opt else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some((w, h)) = original_dims {
|
||||||
|
if !should_use_tiling(*w, *h) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let Some(path) = item.path_opt() else {
|
let Some(path) = item.path_opt() else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
@ -3007,94 +2986,21 @@ impl Tab {
|
||||||
// Clone path to avoid borrow checker issues
|
// Clone path to avoid borrow checker issues
|
||||||
let path = path.to_path_buf();
|
let path = path.to_path_buf();
|
||||||
|
|
||||||
// Clear any previous errors for this image
|
// Try to decode the image using LargeImageManager
|
||||||
self.large_image_manager.clear_error(&path);
|
if self.large_image_manager.try_decode(&path) {
|
||||||
|
vec![Command::Iced(
|
||||||
// Check if image is already decoded or currently decoding
|
cosmic::iced::Task::perform(decode_large_image(path), |result| {
|
||||||
if self.large_image_manager.get_decoded(&path).is_some()
|
result
|
||||||
|| self.large_image_manager.is_decoding(&path)
|
.map(|(path, width, height, pixels)| {
|
||||||
{
|
Message::ImageDecoded(path, width, height, pixels)
|
||||||
return Vec::new();
|
})
|
||||||
|
.unwrap_or_else(|| Message::AutoScroll(None))
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
)]
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to decode the image
|
|
||||||
self.try_decode_image(&path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to decode a large image, handling memory constraints
|
|
||||||
fn try_decode_image(&mut self, path: &PathBuf) -> Vec<Command> {
|
|
||||||
let Some((width, height)) = get_image_dimensions(path) else {
|
|
||||||
self.large_image_manager.store_error(
|
|
||||||
path.clone(),
|
|
||||||
"Failed to read image dimensions".to_string(),
|
|
||||||
);
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
if !self.ensure_memory_available(path, width, height) {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark image as decoding and create the decode task
|
|
||||||
self.large_image_manager.mark_decoding(path.clone());
|
|
||||||
vec![self.create_decode_command(path.clone())]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_memory_available(&mut self, path: &PathBuf, width: u32, height: u32) -> bool {
|
|
||||||
let (has_memory, error_opt) = check_memory_available(width, height);
|
|
||||||
|
|
||||||
if has_memory {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try clearing cache
|
|
||||||
if self.large_image_manager.cache_is_empty() {
|
|
||||||
if let Some(error_msg) = error_opt {
|
|
||||||
self.large_image_manager
|
|
||||||
.store_error(path.clone(), error_msg);
|
|
||||||
log::warn!(
|
|
||||||
"Cannot load {}: insufficient memory and cache is empty",
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Insufficient memory, clearing {} cached images",
|
|
||||||
self.large_image_manager.cache_size()
|
|
||||||
);
|
|
||||||
self.large_image_manager.clear_cache();
|
|
||||||
|
|
||||||
let (has_memory_after_clear, error_opt_after) = check_memory_available(width, height);
|
|
||||||
|
|
||||||
if has_memory_after_clear {
|
|
||||||
log::info!("Memory available after cache clear, proceeding with decode");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(error_msg) = error_opt_after {
|
|
||||||
self.large_image_manager
|
|
||||||
.store_error(path.clone(), error_msg);
|
|
||||||
log::warn!(
|
|
||||||
"Cannot load {}: insufficient memory even after cache clear",
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_decode_command(&self, path: PathBuf) -> Command {
|
|
||||||
Command::Iced(
|
|
||||||
cosmic::iced::Task::perform(decode_large_image(path), |result| {
|
|
||||||
result
|
|
||||||
.map(|(path, width, height, pixels)| {
|
|
||||||
Message::ImageDecoded(path, width, height, pixels)
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| Message::AutoScroll(None))
|
|
||||||
})
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn change_location(&mut self, location: &Location, history_i_opt: Option<usize>) {
|
pub fn change_location(&mut self, location: &Location, history_i_opt: Option<usize>) {
|
||||||
|
|
@ -3556,6 +3462,10 @@ impl Tab {
|
||||||
for (_, item) in &indices {
|
for (_, item) in &indices {
|
||||||
if item.selected && item.can_gallery() {
|
if item.selected && item.can_gallery() {
|
||||||
self.gallery = !self.gallery;
|
self.gallery = !self.gallery;
|
||||||
|
|
||||||
|
if self.gallery {
|
||||||
|
commands.extend(self.trigger_async_decode());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4023,7 +3933,7 @@ impl Tab {
|
||||||
if item.location_opt.as_ref() == Some(&location) {
|
if item.location_opt.as_ref() == Some(&location) {
|
||||||
let handle_opt = match &thumbnail {
|
let handle_opt = match &thumbnail {
|
||||||
ItemThumbnail::NotImage => None,
|
ItemThumbnail::NotImage => None,
|
||||||
ItemThumbnail::Image(handle, _, _) => Some(widget::icon::Handle {
|
ItemThumbnail::Image(handle, _) => Some(widget::icon::Handle {
|
||||||
symbolic: false,
|
symbolic: false,
|
||||||
data: widget::icon::Data::Image(handle.clone()),
|
data: widget::icon::Data::Image(handle.clone()),
|
||||||
}),
|
}),
|
||||||
|
|
@ -4406,50 +4316,58 @@ impl Tab {
|
||||||
.unwrap_or(&ItemThumbnail::NotImage)
|
.unwrap_or(&ItemThumbnail::NotImage)
|
||||||
{
|
{
|
||||||
ItemThumbnail::NotImage => {}
|
ItemThumbnail::NotImage => {}
|
||||||
ItemThumbnail::Image(handle, _original_dims, full_handle_opt) => {
|
ItemThumbnail::Image(handle, original_dims) => {
|
||||||
// Determine which image to show based on async decode state
|
// Determine which image to show based on async decode state
|
||||||
let (image_handle, is_loading, error_msg_opt) = if let Some(path) =
|
let mut is_loading = false;
|
||||||
item.path_opt()
|
let mut error_msg_opt = None;
|
||||||
{
|
let image_handle = if let Some(path) = item.path_opt() {
|
||||||
if let Some(error_msg) = self.large_image_manager.get_error(path) {
|
if let Some(error_msg) = self.large_image_manager.get_error(path) {
|
||||||
(handle, false, Some(error_msg.clone()))
|
error_msg_opt = Some(error_msg.clone());
|
||||||
|
handle.clone()
|
||||||
} else if let Some(decoded_handle) =
|
} else if let Some(decoded_handle) =
|
||||||
self.large_image_manager.get_decoded(path)
|
self.large_image_manager.get_decoded(path)
|
||||||
{
|
{
|
||||||
// Full resolution ready --> use it
|
// Full resolution ready --> use it
|
||||||
(decoded_handle, false, None)
|
decoded_handle.clone()
|
||||||
} else if self.large_image_manager.is_decoding(path) {
|
} else if self.large_image_manager.is_decoding(path) {
|
||||||
// Currently decoding --> show thumbnail with loading indicator
|
// Currently decoding --> show thumbnail with loading indicator
|
||||||
(handle, true, None)
|
is_loading = true;
|
||||||
} else if let Some(full_handle) = full_handle_opt {
|
handle.clone()
|
||||||
// Large image with tiled handle --> use it
|
} else if let Some((w, h)) = original_dims {
|
||||||
(full_handle, false, None)
|
// Check if image needs tiling
|
||||||
|
if should_use_tiling(*w, *h) {
|
||||||
|
// Large image --> show thumbnail only
|
||||||
|
handle.clone()
|
||||||
|
} else {
|
||||||
|
// Normal-sized image --> load full resolution directly
|
||||||
|
widget::image::Handle::from_path(path)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not decoded yet --> show thumbnail
|
// No dimensions available --> show thumbnail
|
||||||
(handle, false, None)
|
handle.clone()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(handle, false, None)
|
handle.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let content: cosmic::Element<'_, Message> =
|
let content: cosmic::Element<'_, Message> =
|
||||||
if let Some(error_msg) = error_msg_opt {
|
if let Some(error_msg) = error_msg_opt {
|
||||||
widget::column()
|
widget::column()
|
||||||
.push(widget::image(image_handle.clone()))
|
.push(widget::image(image_handle))
|
||||||
.push(widget::text(format!("⚠ {}", error_msg)).size(13))
|
.push(widget::text(format!("⚠ {}", error_msg)).size(13))
|
||||||
.padding(space_xs)
|
.padding(space_xs)
|
||||||
.align_x(cosmic::iced::Alignment::Center)
|
.align_x(cosmic::iced::Alignment::Center)
|
||||||
.into()
|
.into()
|
||||||
} else if is_loading {
|
} else if is_loading {
|
||||||
widget::column()
|
widget::column()
|
||||||
.push(widget::image(image_handle.clone()))
|
.push(widget::image(image_handle))
|
||||||
.push(widget::text("Loading full resolution...").size(14))
|
.push(widget::text("Loading full resolution...").size(14))
|
||||||
.padding(space_xs)
|
.padding(space_xs)
|
||||||
.align_x(cosmic::iced::Alignment::Center)
|
.align_x(cosmic::iced::Alignment::Center)
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
//TODO: use widget::image::viewer, when its zoom can be reset
|
//TODO: use widget::image::viewer, when its zoom can be reset
|
||||||
widget::image(image_handle.clone()).into()
|
widget::image(image_handle).into()
|
||||||
};
|
};
|
||||||
|
|
||||||
element_opt =
|
element_opt =
|
||||||
|
|
@ -5884,85 +5802,18 @@ impl Tab {
|
||||||
let max_mb = u64::from(self.thumb_config.max_mem_mb.get());
|
let max_mb = u64::from(self.thumb_config.max_mem_mb.get());
|
||||||
let max_size = u64::from(self.thumb_config.max_size_mb.get());
|
let max_size = u64::from(self.thumb_config.max_size_mb.get());
|
||||||
|
|
||||||
// Determine which queue to use based on image memory requirements.
|
// Determine effective memory budget based on image size
|
||||||
// This routing ensures large images get dedicated worker with full memory budget,
|
let (effective_max_mb, effective_jobs) = if mime.type_() == mime::IMAGE {
|
||||||
// while normal images share 4-worker parallel queue for better throughput.
|
|
||||||
let (use_large_queue, effective_max_mb, effective_jobs) = if mime.type_()
|
|
||||||
== mime::IMAGE
|
|
||||||
{
|
|
||||||
log::debug!("Checking dimensions for image: {}", path.display());
|
|
||||||
match image::image_dimensions(&path) {
|
match image::image_dimensions(&path) {
|
||||||
Ok((width, height)) => {
|
Ok((width, height)) => {
|
||||||
if width == 0 || height == 0 {
|
let (_use_dedicated, eff_mb, eff_jobs) =
|
||||||
log::warn!(
|
should_use_dedicated_worker(width, height, max_mb, max_jobs);
|
||||||
"Invalid image dimensions {}x{} for {}, using normal queue",
|
(eff_mb, eff_jobs)
|
||||||
width,
|
|
||||||
height,
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
(false, max_mb, max_jobs)
|
|
||||||
} else if max_jobs == 0 {
|
|
||||||
log::error!(
|
|
||||||
"Configuration error: max_jobs is 0, using fallback (1 job)"
|
|
||||||
);
|
|
||||||
(true, max_mb, 1)
|
|
||||||
} else {
|
|
||||||
let pixels = (width as u64).saturating_mul(height as u64);
|
|
||||||
let bytes_needed = pixels.saturating_mul(RGBA_BYTES_PER_PIXEL);
|
|
||||||
let mb_needed = bytes_needed / MB_TO_BYTES;
|
|
||||||
|
|
||||||
// Calculate per-job limit with normal queue (shared memory budget)
|
|
||||||
// Use decimal MB conversion to match image crate's limit calculation
|
|
||||||
let total_bytes = max_mb.saturating_mul(DECIMAL_MB_TO_BYTES);
|
|
||||||
let per_job_limit = total_bytes / max_jobs as u64;
|
|
||||||
let per_job_limit_mb = per_job_limit / MB_TO_BYTES;
|
|
||||||
|
|
||||||
log::debug!(
|
|
||||||
"Image {}x{} needs {}MB, per-job limit is {}MB (normal queue)",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
mb_needed,
|
|
||||||
per_job_limit_mb
|
|
||||||
);
|
|
||||||
|
|
||||||
if bytes_needed > per_job_limit {
|
|
||||||
// Image exceeds per-job limit --> route to dedicated large image queue
|
|
||||||
log::info!(
|
|
||||||
"Image {}x{} needs {}MB (exceeds per-job limit of {}MB), \
|
|
||||||
using large image queue (1 worker, {}MB full budget)",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
mb_needed,
|
|
||||||
per_job_limit_mb,
|
|
||||||
max_mb
|
|
||||||
);
|
|
||||||
(true, max_mb, 1)
|
|
||||||
} else {
|
|
||||||
// Image fits in per-job limit --> use normal parallel queue
|
|
||||||
log::debug!(
|
|
||||||
"Image {}x{} fits in normal queue ({}MB < {}MB limit)",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
mb_needed,
|
|
||||||
per_job_limit_mb
|
|
||||||
);
|
|
||||||
(false, max_mb, max_jobs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// Cannot determine size, use normal queue
|
|
||||||
log::debug!(
|
|
||||||
"Failed to get dimensions for {}: {}, using normal queue",
|
|
||||||
path.display(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
(false, max_mb, max_jobs)
|
|
||||||
}
|
}
|
||||||
|
Err(_) => (max_mb, max_jobs),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-image, use normal queue
|
(max_mb, max_jobs)
|
||||||
(false, max_mb, max_jobs)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
subscriptions.push(Subscription::run_with_id(
|
subscriptions.push(Subscription::run_with_id(
|
||||||
|
|
@ -5971,12 +5822,8 @@ impl Tab {
|
||||||
let message = {
|
let message = {
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
|
|
||||||
// Acquire from appropriate semaphore based on image size
|
// Acquire semaphore permit
|
||||||
if use_large_queue {
|
_ = THUMB_SEMAPHORE.acquire().await;
|
||||||
_ = THUMB_SEMAPHORE_LARGE.acquire().await;
|
|
||||||
} else {
|
|
||||||
_ = THUMB_SEMAPHORE_NORMAL.acquire().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
@ -5990,10 +5837,9 @@ impl Tab {
|
||||||
max_size,
|
max_size,
|
||||||
);
|
);
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"thumbnailed {} in {:?} (queue: {})",
|
"thumbnailed {} in {:?}",
|
||||||
path.display(),
|
path.display(),
|
||||||
start.elapsed(),
|
start.elapsed()
|
||||||
if use_large_queue { "large" } else { "normal" }
|
|
||||||
);
|
);
|
||||||
Message::Thumbnail(path, thumbnail)
|
Message::Thumbnail(path, thumbnail)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue