mirror of
https://codeberg.org/Fl1tzi/microdeck.git
synced 2024-05-16 09:51:59 +00:00
major memory improvement by moving the image cache to fs
This commit is contained in:
parent
af42ffa13b
commit
db03650065
|
@ -19,5 +19,6 @@ rusttype = "0.9.3"
|
|||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
font-loader = "0.11.0"
|
||||
clru = "0.6.1"
|
||||
once_cell = "1.19.0"
|
||||
once_cell = "1.19.0"
|
||||
ring = "0.17"
|
||||
base64 = "0.21"
|
||||
|
|
|
@ -3,11 +3,9 @@ use crate::{
|
|||
modules::{start_module, HostEvent, MODULE_REGISTRY},
|
||||
unwrap_or_error,
|
||||
};
|
||||
use clru::CLruCache;
|
||||
use deck_driver as streamdeck;
|
||||
use hidapi::HidApi;
|
||||
use image::DynamicImage;
|
||||
use std::{collections::HashMap, fmt::Display, num::NonZeroUsize, sync::Arc};
|
||||
use std::{collections::HashMap, fmt::Display, sync::Arc};
|
||||
use streamdeck::{
|
||||
asynchronous::{AsyncStreamDeck, ButtonStateUpdate},
|
||||
info::Kind,
|
||||
|
@ -16,10 +14,7 @@ use streamdeck::{
|
|||
use tokio::{
|
||||
process::Command,
|
||||
runtime::Runtime,
|
||||
sync::{
|
||||
mpsc::{self, error::TrySendError},
|
||||
Mutex,
|
||||
},
|
||||
sync::mpsc::{self, error::TrySendError},
|
||||
};
|
||||
use tracing::{debug, error, info_span, trace, warn};
|
||||
|
||||
|
@ -46,11 +41,6 @@ impl Display for Device {
|
|||
}
|
||||
}
|
||||
|
||||
/// image cache
|
||||
///
|
||||
/// (button, key), image data
|
||||
pub type ImageCache = CLruCache<(u8, u32), Arc<DynamicImage>>;
|
||||
|
||||
/// Handles everything related to a single device
|
||||
pub struct Device {
|
||||
modules: HashMap<u8, ModuleController>,
|
||||
|
@ -61,7 +51,6 @@ pub struct Device {
|
|||
selected_space: Option<String>,
|
||||
is_dead_handle: Arc<std::sync::Mutex<bool>>,
|
||||
serial: String,
|
||||
image_cache: Arc<Mutex<ImageCache>>,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
|
@ -104,9 +93,6 @@ impl Device {
|
|||
selected_space: None,
|
||||
is_dead_handle,
|
||||
serial,
|
||||
image_cache: Arc::new(Mutex::new(CLruCache::new(
|
||||
NonZeroUsize::new(button_count.into()).unwrap(),
|
||||
))),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -147,11 +133,10 @@ impl Device {
|
|||
let ser = self.serial.clone();
|
||||
let dev = self.device.clone();
|
||||
let b = btn.clone();
|
||||
let image_cache = self.image_cache.clone();
|
||||
|
||||
runtime.spawn(async move {
|
||||
start_module(ser, b, *module, dev, module_receiver, image_cache).await
|
||||
});
|
||||
runtime.spawn(
|
||||
async move { start_module(ser, b, *module, dev, module_receiver).await },
|
||||
);
|
||||
}
|
||||
// if the receiver already dropped the listener then just directly insert none.
|
||||
// Optimizes performance because the key_listener just does not try to send the event.
|
||||
|
|
|
@ -5,11 +5,13 @@ use image::imageops;
|
|||
use image::{io::Reader, DynamicImage, ImageBuffer, Rgb, RgbImage};
|
||||
use imageproc::drawing::draw_text_mut;
|
||||
use rusttype::Scale;
|
||||
use std::io;
|
||||
use std::{io, path::Path};
|
||||
use tracing::trace;
|
||||
|
||||
/// Loads an image from a path
|
||||
/// Retrieve an image from a path
|
||||
#[allow(dead_code)]
|
||||
pub fn load_image(path: String) -> io::Result<DynamicImage> {
|
||||
pub fn retrieve_image(path: &Path) -> io::Result<DynamicImage> {
|
||||
trace!("Retrieving image from filesystem");
|
||||
Ok(Reader::open(path)?
|
||||
.decode()
|
||||
.expect("Unable to decode image"))
|
||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -6,6 +6,7 @@ use hidapi::HidApi;
|
|||
use rusttype::Font;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
process::exit,
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
time::Duration,
|
||||
|
@ -24,6 +25,7 @@ mod modules;
|
|||
mod type_definition;
|
||||
|
||||
pub static GLOBAL_FONT: OnceLock<Font> = OnceLock::new();
|
||||
pub static CACHE_DIR: &'static str = "microdeck";
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! skip_if_none {
|
||||
|
@ -64,6 +66,14 @@ fn main() {
|
|||
}
|
||||
};
|
||||
|
||||
// create cache dir
|
||||
if let Some(mut path) = dirs::cache_dir() {
|
||||
path.push(CACHE_DIR);
|
||||
if path.exists() == false {
|
||||
fs::create_dir(path).expect("Could not create cache directory");
|
||||
}
|
||||
}
|
||||
|
||||
// load font
|
||||
let font = match config.global.font_family {
|
||||
Some(ref f) => font_loader::system_fonts::get(
|
||||
|
|
147
src/modules.rs
147
src/modules.rs
|
@ -10,21 +10,24 @@ use self::space::Space;
|
|||
|
||||
// other things
|
||||
use crate::config::{Button, ButtonConfigError};
|
||||
use crate::device::ImageCache;
|
||||
use crate::image_rendering::{load_image, ImageBuilder};
|
||||
use crate::image_rendering::{retrieve_image, ImageBuilder};
|
||||
use ::image::imageops::FilterType;
|
||||
use ::image::io::Reader as ImageReader;
|
||||
use ::image::DynamicImage;
|
||||
use async_trait::async_trait;
|
||||
use base64::engine::{general_purpose, Engine};
|
||||
pub use deck_driver as streamdeck;
|
||||
use dirs::cache_dir;
|
||||
use futures_util::Future;
|
||||
use once_cell::sync::Lazy;
|
||||
use ring::digest;
|
||||
use std::collections::HashMap;
|
||||
use std::{error::Error, pin::Pin, sync::Arc};
|
||||
use std::{error::Error, path::PathBuf, pin::Pin, sync::Arc};
|
||||
use streamdeck::info::ImageFormat;
|
||||
use streamdeck::info::Kind;
|
||||
use streamdeck::AsyncStreamDeck;
|
||||
use streamdeck::StreamDeckError;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
pub static MODULE_REGISTRY: Lazy<ModuleRegistry> = Lazy::new(|| ModuleRegistry::default());
|
||||
|
@ -41,7 +44,7 @@ pub enum HostEvent {
|
|||
pub type ModuleObject = Box<dyn Module + Send + Sync>;
|
||||
pub type ModuleFuture =
|
||||
Pin<Box<dyn Future<Output = Result<ModuleObject, ButtonConfigError>> + Send>>;
|
||||
pub type ModuleInitFunction = fn(Arc<Button>, ModuleCache) -> ModuleFuture;
|
||||
pub type ModuleInitFunction = fn(Arc<Button>) -> ModuleFuture;
|
||||
|
||||
pub type ModuleMap = HashMap<&'static str, ModuleInitFunction>;
|
||||
|
||||
|
@ -83,21 +86,15 @@ pub async fn start_module(
|
|||
module_init_function: ModuleInitFunction,
|
||||
device: Arc<AsyncStreamDeck>,
|
||||
br: ChannelReceiver,
|
||||
image_cache: Arc<Mutex<ImageCache>>,
|
||||
) {
|
||||
debug!("STARTED");
|
||||
let mc = ModuleCache::new(
|
||||
image_cache,
|
||||
button.index,
|
||||
device.kind().key_image_format().size,
|
||||
);
|
||||
let da = DeviceAccess::new(device.clone(), button.index).await;
|
||||
|
||||
// init
|
||||
//
|
||||
// This function should be called after the config was checked,
|
||||
// otherwise it will panic and the module wont be started.
|
||||
let mut module = match module_init_function(button.clone(), mc).await {
|
||||
let mut module = match module_init_function(button.clone()).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => panic!("{}", e),
|
||||
};
|
||||
|
@ -121,63 +118,76 @@ pub async fn start_module(
|
|||
}
|
||||
}
|
||||
|
||||
/// A wrapper around [ImageCache] to provide easy access to values in the device cache
|
||||
pub struct ModuleCache {
|
||||
image_cache: Arc<Mutex<ImageCache>>,
|
||||
button_index: u8,
|
||||
/// Resolution of the deck (required for optimization of storage space)
|
||||
resolution: (usize, usize),
|
||||
/// Loads an image from the system or retrieves it from the cache. If
|
||||
/// the provided image is not already in the cache it will be inserted.
|
||||
#[allow(dead_code)]
|
||||
pub async fn load_image(path: PathBuf, resolution: (usize, usize)) -> Option<DynamicImage> {
|
||||
// hash the image
|
||||
let mut image = tokio::task::spawn_blocking(move || retrieve_image(&path))
|
||||
.await
|
||||
.unwrap()
|
||||
.ok()?;
|
||||
|
||||
let image_hash = hash_image(image.as_bytes());
|
||||
|
||||
if let Some(image) = get_image_from_cache(&image_hash, resolution) {
|
||||
trace!("Cached image is available");
|
||||
return Some(image);
|
||||
}
|
||||
|
||||
let image = tokio::task::spawn_blocking(move || {
|
||||
trace!("Resizing image");
|
||||
image = image.resize_exact(
|
||||
resolution.0 as u32,
|
||||
resolution.1 as u32,
|
||||
FilterType::Lanczos3,
|
||||
);
|
||||
trace!("Resizing finished");
|
||||
let mut path = match cache_dir() {
|
||||
Some(dir) => dir,
|
||||
None => return None, // System does not provide cache
|
||||
};
|
||||
path.push("microdeck");
|
||||
path.push(image_cache_file_name(&image_hash, resolution));
|
||||
// let mut file = File::create(path);
|
||||
// file.write_all(image.as_bytes());
|
||||
// currently just ignore if saving is not possible
|
||||
image.save(path).ok()?;
|
||||
Some(image)
|
||||
})
|
||||
.await
|
||||
.unwrap()?;
|
||||
Some(image.into())
|
||||
}
|
||||
|
||||
impl ModuleCache {
|
||||
pub fn new(
|
||||
image_cache: Arc<Mutex<ImageCache>>,
|
||||
button_index: u8,
|
||||
resolution: (usize, usize),
|
||||
) -> Self {
|
||||
ModuleCache {
|
||||
image_cache,
|
||||
button_index,
|
||||
resolution,
|
||||
}
|
||||
}
|
||||
/// File name for a cached image
|
||||
///
|
||||
/// `<hash>-<height>x<width>`
|
||||
pub fn image_cache_file_name(image_hash: &str, resolution: (usize, usize)) -> String {
|
||||
format!("{}-{}x{}.png", image_hash, resolution.0, resolution.1)
|
||||
}
|
||||
|
||||
/// Load an image from the [ImageCache] or create a new one and insert it into the [ImageCache].
|
||||
/// Returns None if no image was found.
|
||||
///
|
||||
/// index: Provide an index where your data is cached. With this number the value can be
|
||||
/// accessed again. Use [DeviceAccess::get_image_cached()] for just getting the data.
|
||||
#[allow(dead_code)]
|
||||
pub async fn load_image(&mut self, path: String, index: u32) -> Option<Arc<DynamicImage>> {
|
||||
if let Some(image) = self.get_image(index).await {
|
||||
Some(image)
|
||||
} else {
|
||||
trace!("Decoding image");
|
||||
let mut image = tokio::task::spawn_blocking(move || load_image(path))
|
||||
.await
|
||||
.unwrap()
|
||||
.ok()?;
|
||||
image = image.resize_exact(
|
||||
self.resolution.0 as u32,
|
||||
self.resolution.1 as u32,
|
||||
FilterType::Lanczos3,
|
||||
);
|
||||
trace!("Decoding finished");
|
||||
let image = Arc::new(image);
|
||||
let mut data = self.image_cache.lock().await;
|
||||
data.put((self.button_index, index), image.clone());
|
||||
trace!("Wrote data into cache (new size: {})", data.len());
|
||||
drop(data);
|
||||
Some(image.into())
|
||||
}
|
||||
}
|
||||
pub fn hash_image(data: &[u8]) -> String {
|
||||
let mut context = digest::Context::new(&digest::SHA256);
|
||||
context.update(data);
|
||||
let hash = context.finish();
|
||||
general_purpose::STANDARD.encode(hash)
|
||||
}
|
||||
|
||||
/// Just try to retrieve a value from the key (index) in the [ImageCache].
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_image(&self, index: u32) -> Option<Arc<DynamicImage>> {
|
||||
let mut data = self.image_cache.lock().await;
|
||||
data.get(&(self.button_index, index)).cloned()
|
||||
}
|
||||
/// Try to retrieve an image from the cache. Will return None if
|
||||
/// the image was not cached yet (or is not accessible)
|
||||
/// or if the system does not provide a [dirs::cache_dir].
|
||||
#[allow(dead_code)]
|
||||
pub fn get_image_from_cache(image_hash: &str, resolution: (usize, usize)) -> Option<DynamicImage> {
|
||||
let mut path = match cache_dir() {
|
||||
Some(dir) => dir,
|
||||
None => return None, // System does not provide cache
|
||||
};
|
||||
|
||||
path.push("microdeck");
|
||||
path.push(image_cache_file_name(image_hash, resolution));
|
||||
|
||||
Some(ImageReader::open(path).ok()?.decode().ok()?)
|
||||
}
|
||||
|
||||
/// Wrapper to provide easier access to the Deck
|
||||
|
@ -240,14 +250,11 @@ pub trait Module: Sync + Send {
|
|||
///
|
||||
/// This function should **not** panic as the panic will not be catched and therefore would be
|
||||
/// not noticed.
|
||||
async fn new(
|
||||
config: Arc<Button>,
|
||||
mut cache: ModuleCache,
|
||||
) -> Result<ModuleObject, ButtonConfigError>
|
||||
async fn new(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError>
|
||||
where
|
||||
Self: Sized;
|
||||
/// Function for actually running the module and interacting with the device. Errors that
|
||||
/// happen here should be mostly prevented.
|
||||
/// happen here should be mostly prevented as they are not properly handled.
|
||||
async fn run(
|
||||
&mut self,
|
||||
device: DeviceAccess,
|
||||
|
|
|
@ -3,7 +3,6 @@ use super::ButtonConfigError;
|
|||
use super::ChannelReceiver;
|
||||
use super::DeviceAccess;
|
||||
use super::Module;
|
||||
use super::ModuleCache;
|
||||
use super::ModuleObject;
|
||||
use super::ReturnError;
|
||||
use async_trait::async_trait;
|
||||
|
@ -13,10 +12,7 @@ pub struct Blank;
|
|||
|
||||
#[async_trait]
|
||||
impl Module for Blank {
|
||||
async fn new(
|
||||
_config: Arc<Button>,
|
||||
_cache: ModuleCache,
|
||||
) -> Result<ModuleObject, ButtonConfigError> {
|
||||
async fn new(_config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
|
||||
Ok(Box::new(Blank {}))
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,7 @@ use std::sync::Arc;
|
|||
use crate::config::Button;
|
||||
|
||||
use super::{
|
||||
ButtonConfigError, ChannelReceiver, DeviceAccess, HostEvent, Module, ModuleCache, ModuleObject,
|
||||
ReturnError,
|
||||
ButtonConfigError, ChannelReceiver, DeviceAccess, HostEvent, Module, ModuleObject, ReturnError,
|
||||
};
|
||||
|
||||
use crate::image_rendering::wrap_text;
|
||||
|
@ -25,10 +24,7 @@ pub struct Counter {
|
|||
|
||||
#[async_trait]
|
||||
impl Module for Counter {
|
||||
async fn new(
|
||||
config: Arc<Button>,
|
||||
_cache: ModuleCache,
|
||||
) -> Result<ModuleObject, ButtonConfigError> {
|
||||
async fn new(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
|
||||
let title = config.parse_module("title", " ".to_string()).res()?;
|
||||
let title_size = config.parse_module("title_size", 15.0).res()?;
|
||||
let number_size = config.parse_module("number_size", 25.0).res()?;
|
||||
|
|
|
@ -1,38 +1,34 @@
|
|||
use super::load_image;
|
||||
use super::Button;
|
||||
use super::ButtonConfigError;
|
||||
use super::ChannelReceiver;
|
||||
use super::DeviceAccess;
|
||||
use super::Module;
|
||||
use super::ModuleCache;
|
||||
use super::ModuleObject;
|
||||
use super::ReturnError;
|
||||
use crate::image_rendering::ImageBuilder;
|
||||
use async_trait::async_trait;
|
||||
use image::DynamicImage;
|
||||
use std::sync::Arc;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
pub struct Image {
|
||||
image: Arc<DynamicImage>,
|
||||
scale: f32,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Module for Image {
|
||||
async fn new(
|
||||
config: Arc<Button>,
|
||||
mut cache: ModuleCache,
|
||||
) -> Result<ModuleObject, ButtonConfigError> {
|
||||
async fn new(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
|
||||
let path = config.parse_module("path", String::new()).required()?;
|
||||
let scale = config.parse_module("scale", 100.0).res()?;
|
||||
|
||||
let image = cache
|
||||
.load_image(path, 1)
|
||||
.await
|
||||
.ok_or(ButtonConfigError::General(
|
||||
let path = PathBuf::from(path);
|
||||
if path.exists() == false {
|
||||
return Err(ButtonConfigError::General(
|
||||
"Image was not found".to_string(),
|
||||
))?;
|
||||
));
|
||||
};
|
||||
|
||||
Ok(Box::new(Image { image, scale }))
|
||||
Ok(Box::new(Image { scale, path }))
|
||||
}
|
||||
|
||||
async fn run(
|
||||
|
@ -41,9 +37,8 @@ impl Module for Image {
|
|||
_button_receiver: ChannelReceiver,
|
||||
) -> Result<(), ReturnError> {
|
||||
let (h, w) = streamdeck.resolution();
|
||||
let img = (*self.image).clone();
|
||||
let img = ImageBuilder::new(h, w)
|
||||
.set_image(img)
|
||||
.set_image(load_image(self.path.clone(), (h, w)).await.unwrap())
|
||||
.set_image_scale(self.scale)
|
||||
.build();
|
||||
streamdeck.write_img(img).await.unwrap();
|
||||
|
|
|
@ -3,7 +3,6 @@ use super::ButtonConfigError;
|
|||
use super::ChannelReceiver;
|
||||
use super::DeviceAccess;
|
||||
use super::Module;
|
||||
use super::ModuleCache;
|
||||
use super::ModuleObject;
|
||||
use super::ReturnError;
|
||||
use crate::image_rendering::{create_error_image, ImageBuilder};
|
||||
|
@ -17,10 +16,7 @@ pub struct Space {
|
|||
|
||||
#[async_trait]
|
||||
impl Module for Space {
|
||||
async fn new(
|
||||
config: Arc<Button>,
|
||||
_cache: ModuleCache,
|
||||
) -> Result<ModuleObject, ButtonConfigError> {
|
||||
async fn new(config: Arc<Button>) -> Result<ModuleObject, ButtonConfigError> {
|
||||
let name = config.parse_module("name", "Unknown".to_string()).res()?;
|
||||
Ok(Box::new(Space { name }))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue