From 1a6454ee79105ea6fffd7e1285a659e4da24af0d Mon Sep 17 00:00:00 2001 From: wonfen Date: Sat, 21 Jun 2025 10:04:01 +0800 Subject: [PATCH] perf: optimize profile switching logic with interrupt support to prevent freeze --- .github/FUNDING.yml | 2 +- src-tauri/src/cmd/profile.rs | 201 +++++++++++++++- src/components/profile/profile-item.tsx | 23 +- src/hooks/use-profiles.ts | 34 ++- src/locales/en.json | 1 + src/locales/zh.json | 1 + src/pages/profiles.tsx | 307 ++++++++++++++++++++---- 7 files changed, 496 insertions(+), 73 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 1120beaf..6a4b4b25 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: clash-verge-rev \ No newline at end of file +github: clash-verge-rev diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 36de5997..0cbc4de0 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -6,12 +6,31 @@ use crate::{ utils::{dirs, help, logging::Type}, wrap_err, }; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; -use tokio::sync::Mutex; +use tokio::sync::{Mutex, RwLock}; -// 添加全局互斥锁防止并发配置更新 +// 全局互斥锁防止并发配置更新 static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(()); +// 全局请求序列号跟踪,用于避免队列化执行 +static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0); + +static CURRENT_PROCESSING_PROFILE: RwLock> = RwLock::const_new(None); + +/// 清理配置处理状态 +async fn cleanup_processing_state(sequence: u64, reason: &str) { + *CURRENT_PROCESSING_PROFILE.write().await = None; + logging!( + info, + Type::Cmd, + true, + "{},清理状态,序列号: {}", + reason, + sequence + ); +} + /// 获取配置文件避免锁竞争 #[tauri::command] pub async fn get_profiles() -> CmdResult { @@ -151,10 +170,60 @@ pub async fn delete_profile(index: String) -> CmdResult { /// 修改profiles的配置 #[tauri::command] pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { - // 获取互斥锁,防止并发执行 - let _guard = PROFILE_UPDATE_MUTEX.lock().await; + // 为当前请求分配序列号 + let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1; + let target_profile = profiles.current.clone(); - logging!(info, Type::Cmd, true, "开始修改配置文件"); + logging!( + info, + Type::Cmd, + true, + "开始修改配置文件,请求序列号: {}, 目标profile: {:?}", + current_sequence, + target_profile + ); + + let mutex_result = + tokio::time::timeout(Duration::from_millis(100), PROFILE_UPDATE_MUTEX.lock()).await; + + let _guard = match mutex_result { + Ok(guard) => guard, + Err(_) => { + let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); + if current_sequence < latest_sequence { + logging!( + info, + Type::Cmd, + true, + "检测到更新的请求 (序列号: {} < {}),放弃当前请求", + current_sequence, + latest_sequence + ); + return Ok(false); + } + logging!( + info, + Type::Cmd, + true, + "强制获取锁以处理最新请求: {}", + current_sequence + ); + PROFILE_UPDATE_MUTEX.lock().await + } + }; + + let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); + if current_sequence < latest_sequence { + logging!( + info, + Type::Cmd, + true, + "获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求", + current_sequence, + latest_sequence + ); + return Ok(false); + } // 保存当前配置,以便在验证失败时恢复 let current_profile = Config::profiles().latest().current.clone(); @@ -269,14 +338,68 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { } } + // 检查请求有效性 + let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); + if current_sequence < latest_sequence { + logging!( + info, + Type::Cmd, + true, + "在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求", + current_sequence, + latest_sequence + ); + return Ok(false); + } + + if let Some(ref profile) = target_profile { + *CURRENT_PROCESSING_PROFILE.write().await = Some(profile.clone()); + logging!( + info, + Type::Cmd, + true, + "设置当前处理profile: {}, 序列号: {}", + profile, + current_sequence + ); + } + // 更新profiles配置 - logging!(info, Type::Cmd, true, "正在更新配置草稿"); + logging!( + info, + Type::Cmd, + true, + "正在更新配置草稿,序列号: {}", + current_sequence + ); let current_value = profiles.current.clone(); let _ = Config::profiles().draft().patch_config(profiles); + // 在调用内核前再次验证请求有效性 + let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); + if current_sequence < latest_sequence { + logging!( + info, + Type::Cmd, + true, + "在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求", + current_sequence, + latest_sequence + ); + Config::profiles().discard(); + return Ok(false); + } + // 为配置更新添加超时保护 + logging!( + info, + Type::Cmd, + true, + "开始内核配置更新,序列号: {}", + current_sequence + ); let update_result = tokio::time::timeout( Duration::from_secs(30), // 30秒超时 CoreManager::global().update_config(), @@ -286,7 +409,28 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { // 更新配置并进行验证 match update_result { Ok(Ok((true, _))) => { - logging!(info, Type::Cmd, true, "配置更新成功"); + // 内核操作完成后再次检查请求有效性 + let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); + if current_sequence < latest_sequence { + logging!( + info, + Type::Cmd, + true, + "内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果", + current_sequence, + latest_sequence + ); + Config::profiles().discard(); + return Ok(false); + } + + logging!( + info, + Type::Cmd, + true, + "配置更新成功,序列号: {}", + current_sequence + ); Config::profiles().apply(); handle::Handle::refresh_clash(); @@ -314,10 +458,19 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { // 立即通知前端配置变更 if let Some(current) = ¤t_value { - logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current); + logging!( + info, + Type::Cmd, + true, + "向前端发送配置变更事件: {}, 序列号: {}", + current, + current_sequence + ); handle::Handle::notify_profile_changed(current.clone()); } + cleanup_processing_state(current_sequence, "配置切换完成").await; + Ok(true) } Ok(Ok((false, error_msg))) => { @@ -351,18 +504,38 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { // 发送验证错误通知 handle::Handle::notice_message("config_validate::error", &error_msg); + + cleanup_processing_state(current_sequence, "配置验证失败").await; + Ok(false) } Ok(Err(e)) => { - logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e); + logging!( + warn, + Type::Cmd, + true, + "更新过程发生错误: {}, 序列号: {}", + e, + current_sequence + ); Config::profiles().discard(); handle::Handle::notice_message("config_validate::boot_error", e.to_string()); + + cleanup_processing_state(current_sequence, "更新过程错误").await; + Ok(false) } Err(_) => { // 超时处理 let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞"; - logging!(error, Type::Cmd, true, "{}", timeout_msg); + logging!( + error, + Type::Cmd, + true, + "{}, 序列号: {}", + timeout_msg, + current_sequence + ); Config::profiles().discard(); if let Some(prev_profile) = current_profile { @@ -370,8 +543,9 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { info, Type::Cmd, true, - "超时后尝试恢复到之前的配置: {}", - prev_profile + "超时后尝试恢复到之前的配置: {}, 序列号: {}", + prev_profile, + current_sequence ); let restore_profiles = IProfiles { current: Some(prev_profile), @@ -382,6 +556,9 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { } handle::Handle::notice_message("config_validate::timeout", timeout_msg); + + cleanup_processing_state(current_sequence, "配置更新超时").await; + Ok(false) } } diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 698d6042..b9183618 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -463,7 +463,15 @@ export const ProfileItem = (props: Props) => { > onSelect(false)} + onClick={(e) => { + // 如果正在激活中,阻止重复点击 + if (activating) { + e.preventDefault(); + e.stopPropagation(); + return; + } + onSelect(false); + }} onContextMenu={(event) => { const { clientX, clientY } = event; setPosition({ top: clientY, left: clientX }); @@ -484,9 +492,16 @@ export const ProfileItem = (props: Props) => { bottom: 2, zIndex: 10, backdropFilter: "blur(2px)", + backgroundColor: "rgba(0, 0, 0, 0.1)", }} > - + )} @@ -535,6 +550,10 @@ export const ProfileItem = (props: Props) => { disabled={loading} onClick={(e) => { e.stopPropagation(); + // 如果正在激活或加载中,阻止更新操作 + if (activating || loading) { + return; + } onUpdate(1); }} > diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts index f94aceb9..836ddb44 100644 --- a/src/hooks/use-profiles.ts +++ b/src/hooks/use-profiles.ts @@ -19,21 +19,29 @@ export const useProfiles = () => { }, ); - const patchProfiles = async (value: Partial) => { - // 立即更新本地状态 - if (value.current && profiles) { - const optimisticUpdate = { - ...profiles, - current: value.current, - }; - mutateProfiles(optimisticUpdate, false); // 不重新验证 - } - + const patchProfiles = async ( + value: Partial, + signal?: AbortSignal, + ) => { try { - await patchProfilesConfig(value); - mutateProfiles(); + if (signal?.aborted) { + throw new DOMException("Operation was aborted", "AbortError"); + } + const success = await patchProfilesConfig(value); + + if (signal?.aborted) { + throw new DOMException("Operation was aborted", "AbortError"); + } + + await mutateProfiles(); + + return success; } catch (error) { - mutateProfiles(); + if (error instanceof DOMException && error.name === "AbortError") { + throw error; + } + + await mutateProfiles(); throw error; } }; diff --git a/src/locales/en.json b/src/locales/en.json index 2fde4d43..eb3f8c6e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -392,6 +392,7 @@ "Profile Imported Successfully": "Profile Imported Successfully", "Profile Switched": "Profile Switched", "Profile Reactivated": "Profile Reactivated", + "Profile switch interrupted by new selection": "Profile switch interrupted by new selection", "Only YAML Files Supported": "Only YAML Files Supported", "Settings Applied": "Settings Applied", "Installing Service...": "Installing Service...", diff --git a/src/locales/zh.json b/src/locales/zh.json index c85653f3..4689bfe6 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -392,6 +392,7 @@ "Profile Imported Successfully": "导入订阅成功", "Profile Switched": "订阅已切换", "Profile Reactivated": "订阅已激活", + "Profile switch interrupted by new selection": "配置切换被新选择中断", "Only YAML Files Supported": "仅支持 YAML 文件", "Settings Applied": "设置已应用", "Installing Service...": "安装服务中...", diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 6bbcd7d5..7d75e401 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -55,6 +55,44 @@ import { listen } from "@tauri-apps/api/event"; import { TauriEvent } from "@tauri-apps/api/event"; import { showNotice } from "@/services/noticeService"; +// 记录profile切换状态 +const debugProfileSwitch = (action: string, profile: string, extra?: any) => { + const timestamp = new Date().toISOString().substring(11, 23); + console.log( + `[Profile-Debug][${timestamp}] ${action}: ${profile}`, + extra || "", + ); +}; + +// 检查请求是否已过期 +const isRequestOutdated = ( + currentSequence: number, + requestSequenceRef: any, + profile: string, +) => { + if (currentSequence !== requestSequenceRef.current) { + debugProfileSwitch( + "REQUEST_OUTDATED", + profile, + `当前序列号: ${currentSequence}, 最新序列号: ${requestSequenceRef.current}`, + ); + return true; + } + return false; +}; + +// 检查是否被中断 +const isOperationAborted = ( + abortController: AbortController, + profile: string, +) => { + if (abortController.signal.aborted) { + debugProfileSwitch("OPERATION_ABORTED", profile); + return true; + } + return false; +}; + const ProfilePage = () => { const { t } = useTranslation(); const location = useLocation(); @@ -63,6 +101,55 @@ const ProfilePage = () => { const [disabled, setDisabled] = useState(false); const [activatings, setActivatings] = useState([]); const [loading, setLoading] = useState(false); + + // 防止重复切换 + const switchingProfileRef = useRef(null); + + // 支持中断当前切换操作 + const abortControllerRef = useRef(null); + + // 只处理最新的切换请求 + const requestSequenceRef = useRef(0); + + // 待处理请求跟踪,取消排队的请求 + const pendingRequestRef = useRef | null>(null); + + // 处理profile切换中断 + const handleProfileInterrupt = ( + previousSwitching: string, + newProfile: string, + ) => { + debugProfileSwitch( + "INTERRUPT_PREVIOUS", + previousSwitching, + `被 ${newProfile} 中断`, + ); + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + debugProfileSwitch("ABORT_CONTROLLER_TRIGGERED", previousSwitching); + } + + if (pendingRequestRef.current) { + debugProfileSwitch("CANCEL_PENDING_REQUEST", previousSwitching); + } + + setActivatings((prev) => prev.filter((id) => id !== previousSwitching)); + showNotice( + "info", + `${t("Profile switch interrupted by new selection")}: ${previousSwitching} → ${newProfile}`, + 3000, + ); + }; + + // 清理切换状态 + const cleanupSwitchState = (profile: string, sequence: number) => { + setActivatings((prev) => prev.filter((id) => id !== profile)); + switchingProfileRef.current = null; + abortControllerRef.current = null; + pendingRequestRef.current = null; + debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`); + }; const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -190,57 +277,165 @@ const ProfilePage = () => { } }; - const activateProfile = useLockFn( - async (profile: string, notifySuccess: boolean) => { - if (profiles.current === profile && !notifySuccess) { - console.log( - `[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`, + const executeBackgroundTasks = async ( + profile: string, + sequence: number, + abortController: AbortController, + ) => { + try { + if ( + sequence === requestSequenceRef.current && + switchingProfileRef.current === profile && + !abortController.signal.aborted + ) { + await activateSelected(); + console.log(`[Profile] 后台处理完成,序列号: ${sequence}`); + } else { + debugProfileSwitch( + "BACKGROUND_TASK_SKIPPED", + profile, + `序列号过期或被中断: ${sequence} vs ${requestSequenceRef.current}`, ); + } + } catch (err: any) { + console.warn("Failed to activate selected proxies:", err); + } + }; + + const activateProfile = async (profile: string, notifySuccess: boolean) => { + if (profiles.current === profile && !notifySuccess) { + console.log(`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`); + return; + } + + const currentSequence = ++requestSequenceRef.current; + debugProfileSwitch("NEW_REQUEST", profile, `序列号: ${currentSequence}`); + + // 处理中断逻辑 + const previousSwitching = switchingProfileRef.current; + if (previousSwitching && previousSwitching !== profile) { + handleProfileInterrupt(previousSwitching, profile); + } + + // 防止重复切换同一个profile + if (switchingProfileRef.current === profile) { + debugProfileSwitch("DUPLICATE_SWITCH_BLOCKED", profile); + return; + } + + // 初始化切换状态 + switchingProfileRef.current = profile; + debugProfileSwitch("SWITCH_START", profile, `序列号: ${currentSequence}`); + + const currentAbortController = new AbortController(); + abortControllerRef.current = currentAbortController; + + setActivatings((prev) => { + if (prev.includes(profile)) return prev; + return [...prev, profile]; + }); + + try { + console.log( + `[Profile] 开始切换到: ${profile},序列号: ${currentSequence}`, + ); + + // 检查请求有效性 + if ( + isRequestOutdated(currentSequence, requestSequenceRef, profile) || + isOperationAborted(currentAbortController, profile) + ) { return; } - // 避免大多数情况下loading态闪烁 - const reset = setTimeout(() => { - setActivatings((prev) => [...prev, profile]); - }, 100); + // 执行切换请求 + const requestPromise = patchProfiles( + { current: profile }, + currentAbortController.signal, + ); + pendingRequestRef.current = requestPromise; - try { - console.log(`[Profile] 开始切换到: ${profile}`); + const success = await requestPromise; - const success = await patchProfiles({ current: profile }); - await mutateLogs(); - closeAllConnections(); - - if (notifySuccess && success) { - showNotice("success", t("Profile Switched"), 1000); - } - - // 立即清除loading状态 - clearTimeout(reset); - setActivatings([]); - - console.log(`[Profile] 切换到 ${profile} 完成,开始后台处理`); - - setTimeout(async () => { - try { - await activateSelected(); - console.log(`[Profile] 后台处理完成`); - } catch (err: any) { - console.warn("Failed to activate selected proxies:", err); - } - }, 50); - } catch (err: any) { - console.error(`[Profile] 切换失败:`, err); - showNotice("error", err?.message || err.toString(), 4000); - clearTimeout(reset); - setActivatings([]); + if (pendingRequestRef.current === requestPromise) { + pendingRequestRef.current = null; } - }, - ); - const onSelect = useLockFn(async (current: string, force: boolean) => { - if (!force && current === profiles.current) return; + + // 再次检查有效性 + if ( + isRequestOutdated(currentSequence, requestSequenceRef, profile) || + isOperationAborted(currentAbortController, profile) + ) { + return; + } + + // 完成切换 + await mutateLogs(); + closeAllConnections(); + + if (notifySuccess && success) { + showNotice("success", t("Profile Switched"), 1000); + } + + console.log( + `[Profile] 切换到 ${profile} 完成,序列号: ${currentSequence},开始后台处理`, + ); + + // 延迟执行后台任务 + setTimeout( + () => + executeBackgroundTasks( + profile, + currentSequence, + currentAbortController, + ), + 50, + ); + } catch (err: any) { + if (pendingRequestRef.current) { + pendingRequestRef.current = null; + } + + // 检查是否因为中断或过期而出错 + if ( + isOperationAborted(currentAbortController, profile) || + isRequestOutdated(currentSequence, requestSequenceRef, profile) + ) { + return; + } + + console.error(`[Profile] 切换失败:`, err); + showNotice("error", err?.message || err.toString(), 4000); + } finally { + // 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态 + if ( + switchingProfileRef.current === profile && + currentSequence === requestSequenceRef.current + ) { + cleanupSwitchState(profile, currentSequence); + } else { + debugProfileSwitch( + "CLEANUP_SKIPPED", + profile, + `序列号不匹配或已被接管: ${currentSequence} vs ${requestSequenceRef.current}`, + ); + } + } + }; + const onSelect = async (current: string, force: boolean) => { + // 阻止重复点击或已激活的profile + if (switchingProfileRef.current === current) { + debugProfileSwitch("DUPLICATE_CLICK_IGNORED", current); + return; + } + + if (!force && current === profiles.current) { + debugProfileSwitch("ALREADY_CURRENT_IGNORED", current); + return; + } + await activateProfile(current, true); - }); + }; useEffect(() => { (async () => { @@ -252,7 +447,16 @@ const ProfilePage = () => { }, current); const onEnhance = useLockFn(async (notifySuccess: boolean) => { - setActivatings(currentActivatings()); + if (switchingProfileRef.current) { + console.log( + `[Profile] 有profile正在切换中(${switchingProfileRef.current}),跳过enhance操作`, + ); + return; + } + + const currentProfiles = currentActivatings(); + setActivatings((prev) => [...new Set([...prev, ...currentProfiles])]); + try { await enhanceProfiles(); mutateLogs(); @@ -262,7 +466,10 @@ const ProfilePage = () => { } catch (err: any) { showNotice("error", err.message || err.toString(), 3000); } finally { - setActivatings([]); + // 保留正在切换的profile,清除其他状态 + setActivatings((prev) => + prev.filter((id) => id === switchingProfileRef.current), + ); } }); @@ -366,6 +573,16 @@ const ProfilePage = () => { }; }, [mutateProfiles]); + // 组件卸载时清理中断控制器 + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + debugProfileSwitch("COMPONENT_UNMOUNT_CLEANUP", "all"); + } + }; + }, []); + return (