diff --git a/Cargo.lock b/Cargo.lock index 5151e50ed..b6c628b90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1215,6 +1215,7 @@ dependencies = [ "futures", "gethostname", "getrandom 0.3.4", + "governor", "log", "nanoid", "network-interface", @@ -2742,6 +2743,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -3100,6 +3107,29 @@ dependencies = [ "system-deps", ] +[[package]] +name = "governor" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" +dependencies = [ + "cfg-if", + "dashmap 6.1.0", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.9.2", + "smallvec", + "spinning_top", + "web-time", +] + [[package]] name = "gtk" version = "0.18.2" @@ -4649,6 +4679,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "normpath" version = "1.5.0" @@ -5904,6 +5940,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -6117,6 +6168,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -7235,6 +7295,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" diff --git a/Changelog.md b/Changelog.md index 7aaf9a786..539341aca 100644 --- a/Changelog.md +++ b/Changelog.md @@ -28,5 +28,6 @@ - 性能优化前后端在渲染流量图时的资源 - 在 Linux NVIDIA 显卡环境下尝试禁用 WebKit DMABUF 渲染以规避潜在问题 - Windows 下自启动改为计划任务实现 +- 改进托盘和窗口操作频率限制实现 diff --git a/crates/tauri-plugin-clash-verge-sysinfo/src/lib.rs b/crates/tauri-plugin-clash-verge-sysinfo/src/lib.rs index fb1dd97a0..3d8642145 100644 --- a/crates/tauri-plugin-clash-verge-sysinfo/src/lib.rs +++ b/crates/tauri-plugin-clash-verge-sysinfo/src/lib.rs @@ -132,6 +132,13 @@ pub fn set_app_core_mode(app: &tauri::AppHandle, mode: impl Into< spec.appinfo.app_core_mode = mode.into(); } +#[inline] +pub fn get_app_uptime(app: &tauri::AppHandle) -> Instant { + let platform_spec = app.state::>(); + let spec = platform_spec.read(); + spec.appinfo.app_startup_time +} + #[inline] pub fn is_current_app_handle_admin(app: &tauri::AppHandle) -> bool { let platform_spec = app.state::>(); diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0d995eff6..275ac65b6 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -100,6 +100,7 @@ clash_verge_service_ipc = { version = "2.0.26", features = [ arc-swap = "1.7.1" rust_iso3166 = "0.1.14" dark-light = "2.0.0" +governor = "0.10.4" [target.'cfg(windows)'.dependencies] deelevate = { workspace = true } diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 160ba408c..4418e06e1 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -1,4 +1,4 @@ -use once_cell::sync::OnceCell; +use governor::{DefaultDirectRateLimiter, Quota, RateLimiter}; use tauri::tray::TrayIconBuilder; use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin; use tauri_plugin_mihomo::models::Proxies; @@ -18,14 +18,11 @@ use crate::{ use super::handle; use anyhow::Result; -use parking_lot::Mutex; use smartstring::alias::String; use std::collections::HashMap; +use std::num::NonZeroU32; use std::sync::Arc; -use std::{ - sync::atomic::{AtomicBool, Ordering}, - time::{Duration, Instant}, -}; +use std::time::Duration; use tauri::{ AppHandle, Wry, menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}, @@ -38,45 +35,13 @@ use menu_def::{MenuIds, MenuTexts}; type ProxyMenuItem = (Option>, Vec>>); +const TRAY_CLICK_DEBOUNCE_MS: u64 = 1_275; + #[derive(Clone)] struct TrayState {} -// 托盘点击防抖机制 -static TRAY_CLICK_DEBOUNCE: OnceCell> = OnceCell::new(); -const TRAY_CLICK_DEBOUNCE_MS: u64 = 300; - -fn get_tray_click_debounce() -> &'static Mutex { - TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1))) -} - -fn should_handle_tray_click() -> bool { - let debounce_lock = get_tray_click_debounce(); - let now = Instant::now(); - - if now.duration_since(*debounce_lock.lock()) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) { - *debounce_lock.lock() = now; - true - } else { - logging!( - debug, - Type::Tray, - "托盘点击被防抖机制忽略,距离上次点击 {}ms", - now.duration_since(*debounce_lock.lock()).as_millis() - ); - false - } -} - -#[cfg(target_os = "macos")] pub struct Tray { - last_menu_update: Mutex>, - menu_updating: AtomicBool, -} - -#[cfg(not(target_os = "macos"))] -pub struct Tray { - last_menu_update: Mutex>, - menu_updating: AtomicBool, + limiter: DefaultDirectRateLimiter, } impl TrayState { @@ -159,10 +124,14 @@ impl TrayState { } impl Default for Tray { + #[allow(clippy::unwrap_used)] fn default() -> Self { Self { - last_menu_update: Mutex::new(None), - menu_updating: AtomicBool::new(false), + limiter: RateLimiter::direct( + Quota::with_period(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS)) + .unwrap() + .allow_burst(NonZeroU32::new(1).unwrap()), + ), } } } @@ -224,45 +193,8 @@ impl Tray { logging!(debug, Type::Tray, "应用正在退出,跳过托盘菜单更新"); return Ok(()); } - // 调整最小更新间隔,确保状态及时刷新 - const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100); - - // 检查是否正在更新 - if self.menu_updating.load(Ordering::Acquire) { - return Ok(()); - } - - // 检查更新频率,但允许重要事件跳过频率限制 - let should_force_update = match std::thread::current().name() { - Some("main") => true, - _ => { - let last_update = self.last_menu_update.lock(); - if let Some(last_time) = *last_update { - last_time.elapsed() >= MIN_UPDATE_INTERVAL - } else { - true - } - } - }; - - if !should_force_update { - return Ok(()); - } - let app_handle = handle::Handle::app_handle(); - - // 设置更新状态 - self.menu_updating.store(true, Ordering::Release); - - let result = self.update_menu_internal(app_handle).await; - - { - let mut last_update = self.last_menu_update.lock(); - *last_update = Some(Instant::now()); - } - self.menu_updating.store(false, Ordering::Release); - - result + self.update_menu_internal(app_handle).await } async fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> { @@ -503,8 +435,8 @@ impl Tray { } = event { // 添加防抖检查,防止快速连击 - if !should_handle_tray_click() { - logging!(info, Type::Tray, "click tray icon too fast, ignore"); + #[allow(clippy::use_self)] + if !Tray::global().should_handle_tray_click() { return; } AsyncHandler::spawn(|| async move { @@ -530,6 +462,14 @@ impl Tray { tray.on_menu_event(on_menu_event); Ok(()) } + + fn should_handle_tray_click(&self) -> bool { + let res = self.limiter.check().is_ok(); + if !res { + logging!(debug, Type::Tray, "tray click rate limited"); + } + res + } } fn create_hotkeys(hotkeys: &Option>) -> HashMap { @@ -1001,10 +941,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { } MenuIds::DASHBOARD => { logging!(info, Type::Tray, "托盘菜单点击: 打开窗口"); - - if !should_handle_tray_click() { - return; - } if !lightweight::exit_lightweight_mode().await { WindowManager::show_main_window().await; }; @@ -1040,9 +976,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { MenuIds::RESTART_CLASH => feat::restart_clash_core().await, MenuIds::RESTART_APP => feat::restart_app().await, MenuIds::LIGHTWEIGHT_MODE => { - if !should_handle_tray_click() { - return; - } if !is_in_lightweight_mode() { lightweight::entry_lightweight_mode().await; } else { diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index 28162ebc8..c008efb64 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -1,17 +1,12 @@ use crate::{core::handle, utils::resolve::window::build_new_window}; use clash_verge_logging::{Type, logging}; -use std::future::Future; +use governor::{DefaultDirectRateLimiter, Quota, RateLimiter}; +use once_cell::sync::Lazy; +use std::num::NonZeroU32; use std::pin::Pin; +use std::time::Duration; use tauri::{Manager as _, WebviewWindow, Wry}; -use once_cell::sync::OnceCell; -use parking_lot::Mutex; -use scopeguard; -use std::{ - sync::atomic::{AtomicBool, Ordering}, - time::{Duration, Instant}, -}; - /// 窗口操作结果 #[derive(Debug, Clone, Copy, PartialEq)] pub enum WindowOperationResult { @@ -45,53 +40,22 @@ pub enum WindowState { } // 窗口操作防抖机制 -static WINDOW_OPERATION_DEBOUNCE: OnceCell> = OnceCell::new(); -static WINDOW_OPERATION_IN_PROGRESS: AtomicBool = AtomicBool::new(false); -const WINDOW_OPERATION_DEBOUNCE_MS: u64 = 500; - -fn get_window_operation_debounce() -> &'static Mutex { - WINDOW_OPERATION_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1))) -} +const WINDOW_OPERATION_DEBOUNCE_MS: u64 = 1_275; +static WINDOW_OPERATION_LIMITER: Lazy = Lazy::new(|| { + #[allow(clippy::unwrap_used)] + RateLimiter::direct( + Quota::with_period(Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS)) + .unwrap() + .allow_burst(NonZeroU32::new(1).unwrap()), + ) +}); fn should_handle_window_operation() -> bool { - if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) { - logging!(warn, Type::Window, "Warning: [防抖] 窗口操作已在进行中,跳过重复调用"); - return false; + let res = WINDOW_OPERATION_LIMITER.check().is_ok(); + if !res { + logging!(debug, Type::Window, "window operation rate limited"); } - - let debounce_lock = get_window_operation_debounce(); - let mut last_operation = debounce_lock.lock(); - let now = Instant::now(); - let elapsed = now.duration_since(*last_operation); - - logging!( - debug, - Type::Window, - "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)", - elapsed.as_millis(), - WINDOW_OPERATION_DEBOUNCE_MS - ); - - if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) { - *last_operation = now; - drop(last_operation); - WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release); - logging!(info, Type::Window, "[防抖] 窗口操作被允许执行"); - true - } else { - logging!( - warn, - Type::Window, - "Warning: [防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms", - elapsed.as_millis(), - WINDOW_OPERATION_DEBOUNCE_MS - ); - false - } -} - -fn finish_window_operation() { - WINDOW_OPERATION_IN_PROGRESS.store(false, Ordering::Release); + res } /// 统一的窗口管理器 @@ -135,9 +99,6 @@ impl WindowManager { if !should_handle_window_operation() { return WindowOperationResult::NoAction; } - let _guard = scopeguard::guard((), |_| { - finish_window_operation(); - }); logging!(info, Type::Window, "开始智能显示主窗口"); logging!(debug, Type::Window, "{}", Self::get_window_status_info()); @@ -149,7 +110,7 @@ impl WindowManager { logging!(info, Type::Window, "窗口不存在,创建新窗口"); if Self::create_window(true).await { logging!(info, Type::Window, "窗口创建成功"); - std::thread::sleep(std::time::Duration::from_millis(100)); + std::thread::sleep(std::time::Duration::from_millis(50)); WindowOperationResult::Created } else { logging!(warn, Type::Window, "窗口创建失败"); @@ -180,11 +141,7 @@ impl WindowManager { // 防抖检查 if !should_handle_window_operation() { return WindowOperationResult::NoAction; - } - let _guard = scopeguard::guard((), |_| { - finish_window_operation(); - }); - + }; logging!(info, Type::Window, "开始切换主窗口显示状态"); let current_state = Self::get_main_window_state();