refactor: remove thread app list synchronizer as it leaks memory on macOS (#573)

This commit is contained in:
SteveLauC
2025-05-29 17:55:24 +08:00
committed by GitHub
parent baded2af1e
commit 51b0a2a545

View File

@@ -12,7 +12,7 @@ use crate::util::open;
use crate::GLOBAL_TAURI_APP_HANDLE; use crate::GLOBAL_TAURI_APP_HANDLE;
use applications::{App, AppTrait}; use applications::{App, AppTrait};
use async_trait::async_trait; use async_trait::async_trait;
use log::{debug, error, info, warn}; use log::{error, warn};
use pizza_engine::document::FieldType; use pizza_engine::document::FieldType;
use pizza_engine::document::{ use pizza_engine::document::{
Document as PizzaEngineDocument, DraftDoc as PizzaEngineDraftDoc, FieldValue, Document as PizzaEngineDocument, DraftDoc as PizzaEngineDraftDoc, FieldValue,
@@ -24,8 +24,6 @@ use pizza_engine::store::{DiskStore, DiskStoreSnapshot};
use pizza_engine::writer::Writer; use pizza_engine::writer::Writer;
use pizza_engine::{doc, Engine, EngineBuilder}; use pizza_engine::{doc, Engine, EngineBuilder};
use serde_json::Value as Json; use serde_json::Value as Json;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf; use std::path::PathBuf;
use tauri::{async_runtime, AppHandle, Manager, Runtime}; use tauri::{async_runtime, AppHandle, Manager, Runtime};
use tauri_plugin_fs_pro::{icon, metadata, name, IconOptions}; use tauri_plugin_fs_pro::{icon, metadata, name, IconOptions};
@@ -48,7 +46,6 @@ const TAURI_STORE_APP_ALIAS: &str = "app_alias";
const TAURI_STORE_KEY_SEARCH_PATH: &str = "search_path"; const TAURI_STORE_KEY_SEARCH_PATH: &str = "search_path";
const TAURI_STORE_KEY_DISABLED_APP_LIST: &str = "disabled_app_list"; const TAURI_STORE_KEY_DISABLED_APP_LIST: &str = "disabled_app_list";
const THREAD_NAME_APP_SYNCHRONIZER: &str = "local app search - app list synchronizer";
/// We use this as: /// We use this as:
/// ///
@@ -196,6 +193,9 @@ macro_rules! task_exec_try {
}; };
} }
// Fields `engine` and `writer` become unused without app list synchronizer, allow
// this rather than removing these fields as we will bring the synchronizer back.
#[allow(dead_code)]
struct ApplicationSearchSourceState { struct ApplicationSearchSourceState {
engine: Engine<DiskStore>, engine: Engine<DiskStore>,
writer: Writer<DiskStore>, writer: Writer<DiskStore>,
@@ -373,6 +373,10 @@ impl<R: Runtime> Task for SearchApplicationsTask<R> {
/// 2. New search paths have been added by the user /// 2. New search paths have been added by the user
/// ///
/// We use this task to index them. /// We use this task to index them.
//
// This become unused without app list synchronizer, allow this rather than
// removing the task as we will bring the synchronizer back.
#[allow(dead_code)]
struct IndexNewApplicationsTask { struct IndexNewApplicationsTask {
applications: Vec<PizzaEngineDraftDoc>, applications: Vec<PizzaEngineDraftDoc>,
callback: Option<tokio::sync::oneshot::Sender<Result<(), String>>>, callback: Option<tokio::sync::oneshot::Sender<Result<(), String>>>,
@@ -459,123 +463,6 @@ impl ApplicationSearchSource {
register_app_hotkey_upon_start(app_handle.clone())?; register_app_hotkey_upon_start(app_handle.clone())?;
if indexing_applications_result.is_err() {
warn!(
"thread [{}] won't start because indexing applications failed",
THREAD_NAME_APP_SYNCHRONIZER
)
} else {
let app_handle_clone = app_handle.clone();
std::thread::Builder::new()
.name(THREAD_NAME_APP_SYNCHRONIZER.into())
.spawn(move || {
let tokio_rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to start a tokio runtime");
tokio_rt.block_on(async move {
info!("thread [{}] started", THREAD_NAME_APP_SYNCHRONIZER);
loop {
tokio::time::sleep(std::time::Duration::from_secs(60 * 2)).await;
debug!("app list synchronizer working");
let stored_app_list = get_app_list(app_handle_clone.clone())
.await
.expect("failed to fetch the stored app list");
let store = app_handle_clone
.store(TAURI_STORE_DISABLED_APP_LIST_AND_SEARCH_PATH)
.unwrap_or_else(|_e| {
panic!(
"store [{}] not found/loaded",
TAURI_STORE_DISABLED_APP_LIST_AND_SEARCH_PATH
)
});
let search_path_json =
store.get(TAURI_STORE_KEY_SEARCH_PATH).unwrap_or_else(|| {
panic!("key [{}] not found", TAURI_STORE_KEY_SEARCH_PATH)
});
let search_paths: Vec<String> = match search_path_json {
Json::Array(array) => array
.into_iter()
.map(|json| match json {
Json::String(str) => str,
_ => unreachable!("search path should be a string"),
})
.collect(),
_ => unreachable!("search paths should be stored in an array"),
};
let mut current_app_list =
list_app_in(search_paths).unwrap_or_else(|e| {
panic!("failed to fetch app list due to error [{}]", e)
});
// filter out Coco-AI
current_app_list
.retain(|app| app.name != app_handle.package_info().name);
let current_app_list_path_hash_index = {
let mut index = HashMap::new();
for (idx, app) in current_app_list.iter().enumerate() {
index.insert(get_app_path(app), idx);
}
index
};
let current_app_path_list: HashSet<String> =
current_app_list.iter().map(get_app_path).collect();
let stored_app_path_list: HashSet<String> = stored_app_list
.iter()
.map(|app_entry| app_entry.path.clone())
.collect();
let new_apps = current_app_path_list.difference(&stored_app_path_list);
debug!("found new apps [{:?}]", new_apps);
// Synchronize the stored app list
let mut new_apps_pizza_engine_documents = Vec::new();
for new_app_path in new_apps {
let idx =
*current_app_list_path_hash_index.get(new_app_path).unwrap();
let new_app = current_app_list.get(idx).unwrap();
let new_app_name = get_app_name(new_app).await;
let new_app_icon_path =
get_app_icon_path(&app_handle_clone, new_app).await.unwrap();
let new_app_alias = get_app_alias(&app_handle_clone, &new_app_path)
.unwrap_or(String::new());
let new_app_pizza_engine_document = doc!(new_app_path.clone(), {
FIELD_APP_NAME => new_app_name,
FIELD_ICON_PATH => new_app_icon_path,
FIELD_APP_ALIAS => new_app_alias,
}
);
new_apps_pizza_engine_documents.push(new_app_pizza_engine_document);
}
let (callback, wait_for_complete) = tokio::sync::oneshot::channel();
let index_new_apps_task = Box::new(IndexNewApplicationsTask {
applications: new_apps_pizza_engine_documents,
callback: Some(callback),
});
RUNTIME_TX
.get()
.unwrap()
.send(index_new_apps_task)
.expect("rx dropped, pizza runtime could possibly be dead");
wait_for_complete
.await
.expect("tx dropped, pizza runtime could possibly be dead")
.unwrap_or_else(|e| {
panic!("failed to index new apps due to error [{}]", e)
});
}
});
})
.unwrap();
}
Ok(()) Ok(())
} }
} }