feat: add governor crate for rate limiting and improve window/tray operation handling

This commit is contained in:
Tunglies
2025-12-27 20:27:49 +08:00
parent 2c1303c2bd
commit c41db51f81
6 changed files with 120 additions and 152 deletions

69
Cargo.lock generated
View File

@@ -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"

View File

@@ -28,5 +28,6 @@
- 性能优化前后端在渲染流量图时的资源
- 在 Linux NVIDIA 显卡环境下尝试禁用 WebKit DMABUF 渲染以规避潜在问题
- Windows 下自启动改为计划任务实现
- 改进托盘和窗口操作频率限制实现
</details>

View File

@@ -132,6 +132,13 @@ pub fn set_app_core_mode<R: Runtime>(app: &tauri::AppHandle<R>, mode: impl Into<
spec.appinfo.app_core_mode = mode.into();
}
#[inline]
pub fn get_app_uptime<R: Runtime>(app: &tauri::AppHandle<R>) -> Instant {
let platform_spec = app.state::<RwLock<Platform>>();
let spec = platform_spec.read();
spec.appinfo.app_startup_time
}
#[inline]
pub fn is_current_app_handle_admin<R: Runtime>(app: &tauri::AppHandle<R>) -> bool {
let platform_spec = app.state::<RwLock<Platform>>();

View File

@@ -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 }

View File

@@ -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<Submenu<Wry>>, Vec<Box<dyn IsMenuItem<Wry>>>);
const TRAY_CLICK_DEBOUNCE_MS: u64 = 1_275;
#[derive(Clone)]
struct TrayState {}
// 托盘点击防抖机制
static TRAY_CLICK_DEBOUNCE: OnceCell<Mutex<Instant>> = OnceCell::new();
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
fn get_tray_click_debounce() -> &'static Mutex<Instant> {
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<Option<Instant>>,
menu_updating: AtomicBool,
}
#[cfg(not(target_os = "macos"))]
pub struct Tray {
last_menu_update: Mutex<Option<Instant>>,
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<Vec<String>>) -> HashMap<String, String> {
@@ -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 {

View File

@@ -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<Mutex<Instant>> = 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<Instant> {
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<DefaultDirectRateLimiter> = 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();