perf(wallpaper): reduce peak heap allocations by 90%
This commit is contained in:
parent
039658d5fd
commit
096ae80dba
3 changed files with 116 additions and 40 deletions
|
|
@ -67,9 +67,9 @@ pub fn color_image(
|
||||||
/// Color selection list
|
/// Color selection list
|
||||||
pub fn color_select_options() -> Element<'static, Message> {
|
pub fn color_select_options() -> Element<'static, Message> {
|
||||||
flex_select_row(|vec, _size| {
|
flex_select_row(|vec, _size| {
|
||||||
let elements = wallpaper::DEFAULT_COLORS.iter().cloned().map(color_button);
|
for color in wallpaper::DEFAULT_COLORS {
|
||||||
|
vec.push(color_button(color.clone()));
|
||||||
vec.extend(elements);
|
}
|
||||||
|
|
||||||
COLOR_WIDTH
|
COLOR_WIDTH
|
||||||
})
|
})
|
||||||
|
|
@ -78,13 +78,9 @@ pub fn color_select_options() -> Element<'static, Message> {
|
||||||
/// Background selection list
|
/// Background selection list
|
||||||
pub fn wallpaper_select_options(page: &super::Page) -> Element<Message> {
|
pub fn wallpaper_select_options(page: &super::Page) -> Element<Message> {
|
||||||
flex_select_row(move |vec, _size| {
|
flex_select_row(move |vec, _size| {
|
||||||
let elements = page
|
for (id, handle) in &page.selection.selection_handles {
|
||||||
.selection
|
vec.push(wallpaper_button(handle, id));
|
||||||
.selection_handles
|
}
|
||||||
.iter()
|
|
||||||
.map(|(id, handle)| wallpaper_button(handle, id));
|
|
||||||
|
|
||||||
vec.extend(elements);
|
|
||||||
|
|
||||||
WALLPAPER_WIDTH
|
WALLPAPER_WIDTH
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ freedesktop-icons = "0.2.3"
|
||||||
futures-lite = "1.13.0"
|
futures-lite = "1.13.0"
|
||||||
image = "0.24.6"
|
image = "0.24.6"
|
||||||
rayon = "1.7.0"
|
rayon = "1.7.0"
|
||||||
|
sctk = { package = "smithay-client-toolkit", git = "https://github.com/pop-os/client-toolkit", tag = "themed-pointer"}
|
||||||
tokio = { version = "1.28.0", features = ["sync"] }
|
tokio = { version = "1.28.0", features = ["sync"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
sctk = { package = "smithay-client-toolkit", git = "https://github.com/pop-os/client-toolkit", tag = "themed-pointer"}
|
|
||||||
wayland-client = "0.30.1"
|
wayland-client = "0.30.1"
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
pub use cosmic_bg_config::{Color, Config, Entry, Gradient, ScalingMode, Source};
|
pub use cosmic_bg_config::{Color, Config, Entry, Gradient, ScalingMode, Source};
|
||||||
|
|
||||||
use image::RgbaImage;
|
use image::{DynamicImage, RgbaImage};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::{hash_map::DefaultHasher, HashMap},
|
collections::{hash_map::DefaultHasher, HashMap},
|
||||||
fs::DirEntry,
|
fs::DirEntry,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
|
io::Read,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::{self, Receiver};
|
use tokio::sync::mpsc::{self, Receiver};
|
||||||
|
|
||||||
|
|
@ -97,11 +97,12 @@ pub fn cache_dir() -> Option<PathBuf> {
|
||||||
/// Loads wallpapers in parallel by spawning tasks with a rayon thread pool.
|
/// Loads wallpapers in parallel by spawning tasks with a rayon thread pool.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaImage)> {
|
pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaImage)> {
|
||||||
let cache_dir = Arc::new(cache_dir());
|
let cache_dir = cache_dir();
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel(1);
|
let (tx, rx) = mpsc::channel(1);
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
let mut paths = vec![path];
|
let mut paths = vec![path];
|
||||||
|
|
||||||
while let Some(path) = paths.pop() {
|
while let Some(path) = paths.pop() {
|
||||||
|
|
@ -116,19 +117,49 @@ pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaI
|
||||||
if file_type.is_dir() {
|
if file_type.is_dir() {
|
||||||
paths.push(path);
|
paths.push(path);
|
||||||
} else if file_type.is_file() {
|
} else if file_type.is_file() {
|
||||||
let tx = tx.clone();
|
let image_operation =
|
||||||
let cache_dir = cache_dir.clone();
|
load_thumbnail(&mut buffer, cache_dir.as_deref(), &path, &entry);
|
||||||
rayon::spawn_fifo(move || {
|
|
||||||
let display_thumbnail =
|
if let Some(image_operation) = image_operation {
|
||||||
load_thumbnail(cache_dir.as_deref(), &path, &entry, 300, 169);
|
let tokio_handle = tokio::runtime::Handle::current();
|
||||||
|
let tx = tx.clone();
|
||||||
|
|
||||||
|
rayon::spawn_fifo(move || {
|
||||||
|
let display_thumbnail = match image_operation {
|
||||||
|
ImageOperation::Cached(thumbnail) => thumbnail.to_rgba8(),
|
||||||
|
|
||||||
|
ImageOperation::GenerateThumbnail { path, image } => {
|
||||||
|
let image = image.thumbnail(300, 169).to_rgba8();
|
||||||
|
|
||||||
|
if let Some(path) = path {
|
||||||
|
// Save thumbnail to disk without blocking.
|
||||||
|
tokio_handle.spawn_blocking({
|
||||||
|
let image = image.clone();
|
||||||
|
move || {
|
||||||
|
if let Err(why) = image.save(&path) {
|
||||||
|
tracing::error!(
|
||||||
|
?path,
|
||||||
|
?why,
|
||||||
|
"failed to save image thumbnail"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _res = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
image
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(display_thumbnail) = display_thumbnail {
|
|
||||||
let mut selection_thumbnail = image::imageops::resize(
|
let mut selection_thumbnail = image::imageops::resize(
|
||||||
&display_thumbnail,
|
&display_thumbnail,
|
||||||
158,
|
158,
|
||||||
105,
|
105,
|
||||||
image::imageops::FilterType::Lanczos3,
|
image::imageops::FilterType::Lanczos3,
|
||||||
);
|
);
|
||||||
|
|
||||||
round(&mut selection_thumbnail, [8, 8, 8, 8]);
|
round(&mut selection_thumbnail, [8, 8, 8, 8]);
|
||||||
|
|
||||||
let _res = tx.blocking_send((
|
let _res = tx.blocking_send((
|
||||||
|
|
@ -136,8 +167,8 @@ pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaI
|
||||||
display_thumbnail,
|
display_thumbnail,
|
||||||
selection_thumbnail,
|
selection_thumbnail,
|
||||||
));
|
));
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,18 +178,25 @@ pub fn load_each_from_path(path: PathBuf) -> Receiver<(PathBuf, RgbaImage, RgbaI
|
||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates and caches the thumbnail of a wallpaper.
|
enum ImageOperation {
|
||||||
|
GenerateThumbnail {
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
image: DynamicImage,
|
||||||
|
},
|
||||||
|
Cached(DynamicImage),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads wallpaper thumbnails, or defines what needs to be done to create them.
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// Caching reduces time required to load a wallpaper by 99%.
|
/// Caching reduces time required to load a wallpaper by 99%.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn load_thumbnail(
|
fn load_thumbnail(
|
||||||
|
input_buffer: &mut Vec<u8>,
|
||||||
cache_dir: Option<&Path>,
|
cache_dir: Option<&Path>,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
entry: &DirEntry,
|
entry: &DirEntry,
|
||||||
width: u32,
|
) -> Option<ImageOperation> {
|
||||||
height: u32,
|
|
||||||
) -> Option<RgbaImage> {
|
|
||||||
if let Some(cache_dir) = cache_dir {
|
if let Some(cache_dir) = cache_dir {
|
||||||
if let Ok(ctime) = entry.metadata().and_then(|meta| meta.created()) {
|
if let Ok(ctime) = entry.metadata().and_then(|meta| meta.created()) {
|
||||||
// Search for thumbnail by a unique hash string.
|
// Search for thumbnail by a unique hash string.
|
||||||
|
|
@ -169,27 +207,69 @@ pub fn load_thumbnail(
|
||||||
|
|
||||||
let thumbnail_path = cache_dir.join(format!("{hash:x}.png"));
|
let thumbnail_path = cache_dir.join(format!("{hash:x}.png"));
|
||||||
|
|
||||||
// Load image from thumbnail if it exists and can be opened.
|
|
||||||
if thumbnail_path.exists() {
|
if thumbnail_path.exists() {
|
||||||
if let Ok(image) = image::open(&thumbnail_path) {
|
if let Some(image) = open_image(input_buffer, &thumbnail_path) {
|
||||||
return Some(image.into_rgba8());
|
return Some(ImageOperation::Cached(image));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _res = std::fs::remove_file(&thumbnail_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new thumbnail and save it if not.
|
return Some(ImageOperation::GenerateThumbnail {
|
||||||
return image::open(path).ok().map(|mut image| {
|
path: Some(thumbnail_path),
|
||||||
image = image.thumbnail_exact(width, height);
|
image: open_image(input_buffer, path)?,
|
||||||
let _res = image.save(&thumbnail_path);
|
|
||||||
image.into_rgba8()
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate thumbnail from wallpaper without saving it
|
if let Some(image) = open_image(input_buffer, path) {
|
||||||
image::open(path).ok().map(|mut image| {
|
return Some(ImageOperation::GenerateThumbnail { path: None, image });
|
||||||
image = image.thumbnail_exact(width, height);
|
}
|
||||||
image.into_rgba8()
|
|
||||||
})
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_image(input_buffer: &mut Vec<u8>, path: &Path) -> Option<DynamicImage> {
|
||||||
|
let capacity = match path.metadata() {
|
||||||
|
Ok(metadata) => metadata.len() as usize,
|
||||||
|
Err(why) => {
|
||||||
|
tracing::error!(?path, ?why, "error loading image metadata");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input_buffer.clear();
|
||||||
|
input_buffer.reserve_exact(capacity);
|
||||||
|
|
||||||
|
let mut file = match std::fs::File::open(path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(why) => {
|
||||||
|
tracing::error!(?path, ?why, "error opening image");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(why) = file.read_to_end(input_buffer) {
|
||||||
|
tracing::error!(?path, ?why, "error reading image");
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_cursor = std::io::Cursor::new(input_buffer);
|
||||||
|
let mut image_decoder = image::io::Reader::new(input_cursor);
|
||||||
|
|
||||||
|
image_decoder = if let Ok(decoder) = image_decoder.with_guessed_format() {
|
||||||
|
decoder
|
||||||
|
} else {
|
||||||
|
tracing::error!(?path, "unsupported image format");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
match image_decoder.decode() {
|
||||||
|
Ok(image) => Some(image),
|
||||||
|
Err(why) => {
|
||||||
|
tracing::error!(?path, ?why, "image decode failed");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://users.rust-lang.org/t/how-to-trim-image-to-circle-image-without-jaggy/70374/2
|
// https://users.rust-lang.org/t/how-to-trim-image-to-circle-image-without-jaggy/70374/2
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue