perf: replace debounced profile saving with a 30-second polling approach, retaining flushes for user changes (#402)

Co-authored-by: nekename <63245705+nekename@users.noreply.github.com>
This commit is contained in:
Craig McLureandnekename authored and GitHub committed 2026-07-26 21:46:19 +01:00
1 parent 9787e4ef3c
commit 0a0072a648
10 files changed
+76 -59

No files matched your search

+6 -6
View File
@@ -1,7 +1,7 @@
use super::Error;
use crate::shared::{Action, ActionContext, ActionInstance, ActionState, Context, config_dir};
use crate::store::profiles::{LocksMut, acquire_locks, acquire_locks_mut, get_instance_mut, get_slot, get_slot_mut, save_profile};
use crate::store::profiles::{LocksMut, acquire_locks, acquire_locks_mut, get_instance_mut, get_slot, get_slot_mut, save_profile_now};
use tauri::{AppHandle, Emitter, Manager, command};
use tokio::fs::remove_dir_all;
@@ -44,7 +44,7 @@ pub async fn create_instance(app: AppHandle, mut action: Action, context: Contex
let _ = update_state(&app, parent.context.clone(), &mut locks).await;
}
save_profile(&context.device, &mut locks).await?;
save_profile_now(&context.device, &mut locks).await?;
drop(locks);
let _ = crate::events::outbound::will_appear::will_appear(&instance).await;
@@ -68,7 +68,7 @@ pub async fn create_instance(app: AppHandle, mut action: Action, context: Contex
*slot = Some(instance.clone());
let slot = slot.clone();
save_profile(&context.device, &mut locks).await?;
save_profile_now(&context.device, &mut locks).await?;
let _ = crate::events::outbound::will_appear::will_appear(&instance).await;
Ok(slot)
@@ -146,7 +146,7 @@ pub async fn move_instance(source: Context, destination: Context, retain: bool)
let _ = crate::events::outbound::will_appear::will_appear(&new).await;
save_profile(&destination.device, &mut locks).await?;
save_profile_now(&destination.device, &mut locks).await?;
Ok(Some(new))
}
@@ -190,7 +190,7 @@ pub async fn remove_instance(context: ActionContext) -> Result<(), Error> {
}
}
save_profile(&context.device, &mut locks).await?;
save_profile_now(&context.device, &mut locks).await?;
Ok(())
}
@@ -219,7 +219,7 @@ pub async fn set_state(context: ActionContext, index: u16, state: ActionState) -
let reference = get_instance_mut(&context, &mut locks).await?.unwrap();
reference.states[index as usize] = state;
let clone = reference.clone();
save_profile(&context.device, &mut locks).await?;
save_profile_now(&context.device, &mut locks).await?;
crate::events::outbound::states::title_parameters_did_change(&clone, index).await?;
Ok(())
}
+3 -15
View File
@@ -1,7 +1,7 @@
use super::Error;
use crate::shared::DEVICES;
use crate::store::profiles::{PROFILE_SAVE_DEBOUNCE, PROFILE_STORES, acquire_locks_mut, get_device_profiles, save_profile};
use crate::store::profiles::{PROFILE_STORES, acquire_locks_mut, get_device_profiles, save_profile_now};
use tauri::{AppHandle, Emitter, Manager, command};
@@ -32,20 +32,8 @@ pub async fn set_selected_profile(device: String, id: String) -> Result<(), Erro
}
// If a profile save is pending for this device, save it immediately to prevent losing profile data
let entries = PROFILE_SAVE_DEBOUNCE
.iter()
.filter(|entry| entry.key().device == device)
.map(|entry| entry.key().clone())
.collect::<Vec<_>>();
if !entries.is_empty() {
for context in &entries {
if let Some((_, handle)) = PROFILE_SAVE_DEBOUNCE.remove(context) {
handle.abort();
}
}
if let Err(error) = save_profile(&device, &mut locks).await {
log::error!("Failed to save profile for device {device}: {error}");
}
if let Err(error) = save_profile_now(&device, &mut locks).await {
log::error!("Failed to save profile for device {device}: {error}");
}
let selected_profile = locks.device_stores.get_selected_profile(&device)?;
+5
View File
@@ -71,6 +71,11 @@ pub async fn deregister_device(uuid: &str, event: PayloadEvent<String>) -> Resul
let _ = crate::events::outbound::will_appear::will_disappear(instance, false).await;
}
// Flush any pending profile writes before removing the device.
if let Err(error) = crate::store::profiles::save_profile_now(&event.payload, &mut locks).await {
log::error!("Failed to flush profile for device {}: {error}", event.payload);
}
if let Ok(profiles) = get_device_profiles(&event.payload) {
for profile in profiles {
locks.profile_stores.remove_profile(&event.payload, &profile);
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::events::outbound::settings as outbound;
use crate::shared::ActionContext;
use crate::store::profiles::{acquire_locks, acquire_locks_mut, get_instance, get_instance_mut, save_profile};
use crate::store::profiles::{acquire_locks, acquire_locks_mut, get_instance, get_instance_mut, mark_profile_stale};
use std::io::Write;
use std::str::FromStr;
@@ -11,7 +11,7 @@ pub async fn set_settings(event: super::ContextAndPayloadEvent<serde_json::Value
if let Some(instance) = get_instance_mut(&event.context, &mut locks).await? {
instance.settings = event.payload;
outbound::did_receive_settings(instance, !from_property_inspector).await?;
save_profile(&event.context.device, &mut locks).await?;
mark_profile_stale(&event.context.device, &mut locks).await?;
}
Ok(())
+4 -11
View File
@@ -1,7 +1,7 @@
use super::ContextAndPayloadEvent;
use crate::events::frontend::instances::update_state;
use crate::store::profiles::{acquire_locks_mut, debounce_profile_save, get_instance_mut, save_profile};
use crate::store::profiles::{acquire_locks_mut, get_instance_mut, mark_profile_stale};
use anyhow::bail;
use serde::Deserialize;
@@ -59,7 +59,7 @@ pub async fn set_title(event: ContextAndPayloadEvent<SetTitlePayload>) -> Result
}
update_state(crate::APP_HANDLE.get().unwrap(), instance.context.clone(), &mut locks).await?;
}
save_profile(&event.context.device, &mut locks).await?;
mark_profile_stale(&event.context.device, &mut locks).await?;
Ok(())
}
@@ -97,14 +97,7 @@ pub async fn set_image(mut event: ContextAndPayloadEvent<SetImagePayload>) -> Re
update_state(crate::APP_HANDLE.get().unwrap(), instance.context.clone(), &mut locks).await?;
}
if let Some(image) = &event.payload.image
&& image.trim().starts_with("data:")
{
debounce_profile_save(event.context);
} else {
save_profile(&event.context.device, &mut locks).await?;
}
mark_profile_stale(&event.context.device, &mut locks).await?;
Ok(())
}
@@ -148,7 +141,7 @@ pub async fn set_state(event: ContextAndPayloadEvent<SetStatePayload>) -> Result
instance.current_state = event.payload.state;
update_state(crate::APP_HANDLE.get().unwrap(), instance.context.clone(), &mut locks).await?;
}
save_profile(&event.context.device, &mut locks).await?;
mark_profile_stale(&event.context.device, &mut locks).await?;
Ok(())
}
+3 -3
View File
@@ -2,7 +2,7 @@ use super::{GenericInstancePayload, send_to_plugin};
use crate::events::frontend::instances::{key_moved, update_state};
use crate::shared::{ActionContext, Context};
use crate::store::profiles::{acquire_locks_mut, get_slot_mut, save_profile};
use crate::store::profiles::{acquire_locks_mut, get_slot_mut, mark_profile_stale};
use std::sync::LazyLock;
use std::time::Duration;
@@ -75,7 +75,7 @@ pub async fn key_down(device: &str, key: u8) -> Result<(), anyhow::Error> {
let _ = update_state(crate::APP_HANDLE.get().unwrap(), child, &mut locks).await;
}
save_profile(device, &mut locks).await?;
mark_profile_stale(device, &mut locks).await?;
} else if instance.action.uuid == "opendeck.toggleaction" {
let children = instance.children.as_ref().unwrap();
if children.is_empty() {
@@ -168,7 +168,7 @@ pub async fn key_up(device: &str, key: u8) -> Result<(), anyhow::Error> {
};
let _ = update_state(crate::APP_HANDLE.get().unwrap(), instance.context.clone(), &mut locks).await;
save_profile(device, &mut locks).await?;
mark_profile_stale(device, &mut locks).await?;
Ok(())
}
+19
View File
@@ -20,6 +20,7 @@ use events::frontend;
use shared::PRODUCT_NAME;
use std::sync::OnceLock;
use std::time::Duration;
use tauri::{
AppHandle, Builder, Manager, WindowEvent,
@@ -29,6 +30,7 @@ use tauri::{
use tauri_plugin_log::{Target, TargetKind};
static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
const SAVE_PROBE: Duration = Duration::from_secs(30);
fn show_window(app: &AppHandle) -> Result<(), tauri::Error> {
#[cfg(target_os = "macos")]
@@ -196,6 +198,16 @@ If you have already donated, thank you so much for your support!"#,
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
}
});
tokio::spawn(async {
loop {
tokio::time::sleep(SAVE_PROBE).await;
if let Err(error) = store::profiles::flush_stale_profiles().await {
log::error!("Failed to flush stale profiles: {error}");
}
}
});
plugins::initialise_plugins();
application_watcher::init_application_watcher();
device_sleep::init_device_sleep();
@@ -380,6 +392,13 @@ If you have already donated, thank you so much for your support!"#,
if let tauri::RunEvent::Exit = event {
#[cfg(windows)]
futures::executor::block_on(plugins::deactivate_plugins());
let flush_future = store::profiles::flush_stale_profiles();
match tauri::async_runtime::block_on(flush_future) {
Ok(_) => log::info!("Successfully flushed all stale profiles on exit"),
Err(error) => log::error!("Failed to flush stale profiles on exit: {error}"),
}
tokio::spawn(elgato::reset_devices());
use tauri_plugin_aptabase::EventTracker;
app.flush_events_blocking();
+3
View File
@@ -439,6 +439,9 @@ pub struct Profile {
pub sliders: Vec<Option<ActionInstance>>,
#[serde(default)]
pub infobars: Vec<Option<ActionInstance>>,
#[serde(skip)]
pub stale: bool,
}
/// A map of category names to a list of actions in that category.
+29 -22
View File
@@ -8,10 +8,8 @@ use std::path::PathBuf;
use std::sync::LazyLock;
use anyhow::{Context, anyhow};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tokio::task::JoinHandle;
pub struct ProfileStores {
stores: HashMap<String, Store<Profile>>,
@@ -40,6 +38,8 @@ impl ProfileStores {
keys: Vec::new(),
sliders: Vec::new(),
infobars: Vec::new(),
stale: false,
};
let mut store = Store::new(&canonical_id, &config_dir().join("profiles"), default).context(format!("Failed to create store for profile {}", canonical_id))?;
@@ -369,27 +369,34 @@ pub async fn get_instance_mut<'a>(context: &crate::shared::ActionContext, locks:
Ok(None)
}
pub async fn save_profile(device: &str, locks: &mut LocksMut<'_>) -> Result<(), anyhow::Error> {
let selected_profile = locks.device_stores.get_selected_profile(device)?;
let device = DEVICES.get(device).ok_or_else(|| anyhow!("device not found"))?;
let store = locks.profile_stores.get_profile_store(&device, &selected_profile)?;
store.save()
pub async fn mark_profile_stale(device_id: &str, locks: &mut LocksMut<'_>) -> Result<(), anyhow::Error> {
let selected_profile = locks.device_stores.get_selected_profile(device_id)?;
let device = DEVICES.get(device_id).ok_or_else(|| anyhow!("device not found"))?;
let store = locks.profile_stores.get_profile_store_mut(&device, &selected_profile).await?;
store.value.stale = true;
Ok(())
}
pub static PROFILE_SAVE_DEBOUNCE: LazyLock<DashMap<crate::shared::ActionContext, JoinHandle<()>>> = LazyLock::new(DashMap::new);
pub fn debounce_profile_save(context: crate::shared::ActionContext) {
if let Some((_, handle)) = PROFILE_SAVE_DEBOUNCE.remove(&context) {
handle.abort();
pub async fn save_profile_now(device_id: &str, locks: &mut LocksMut<'_>) -> Result<(), anyhow::Error> {
let selected_profile = locks.device_stores.get_selected_profile(device_id)?;
let device = DEVICES.get(device_id).ok_or_else(|| anyhow!("device not found"))?;
let store = locks.profile_stores.get_profile_store_mut(&device, &selected_profile).await?;
if store.value.stale {
store.save()?;
store.value.stale = false;
}
PROFILE_SAVE_DEBOUNCE.insert(
context.clone(),
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let mut locks = acquire_locks_mut().await;
if let Err(error) = save_profile(&context.device, &mut locks).await {
log::error!("Failed to save profile for device {}: {error}", context.device);
}
PROFILE_SAVE_DEBOUNCE.remove(&context);
}),
);
Ok(())
}
pub async fn flush_stale_profiles() -> Result<(), anyhow::Error> {
let mut locks = acquire_locks_mut().await;
for store in locks.profile_stores.stores.values_mut() {
if store.value.stale {
store.save()?;
store.value.stale = false;
}
}
Ok(())
}
@@ -216,6 +216,8 @@ impl DiskProfile {
keys: self.keys.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(),
sliders: self.sliders.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(),
infobars: self.infobars.into_iter().map(|x| x.map(|v| v.into_action_instance(path))).collect(),
stale: false,
}
}
}