From dbb4877be666844ee782426e4790283d551e0836 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Sun, 16 Nov 2025 00:33:21 +0800 Subject: [PATCH] refactor(Draft): management as crate (#5470) * feat: implement draft functionality with apply and discard methods, and add benchmarks and tests * Refactor Draft management and integrate Tokio for asynchronous operations - Introduced a new `IVerge` struct for configuration management. - Updated `Draft` struct to use `Arc` for better concurrency handling. - Added asynchronous editing capabilities to `Draft` using Tokio. - Replaced synchronous editing methods with asynchronous counterparts. - Updated benchmark tests to reflect changes in the `Draft` API. - Removed redundant draft utility module and integrated its functionality into the main `Draft` implementation. - Adjusted tests to validate new behavior and ensure correctness of the `Draft` management flow. --- src-tauri/Cargo.lock | 11 ++ src-tauri/Cargo.toml | 42 ++++--- src-tauri/crates/draft/Cargo.toml | 17 +++ .../draft/bench/benche_me.rs} | 23 +++- src-tauri/crates/draft/src/lib.rs | 102 +++++++++++++++++ .../draft/tests/test_me.rs} | 105 +----------------- src-tauri/src/cmd/verge.rs | 3 +- src-tauri/src/config/config.rs | 3 +- src-tauri/src/feat/config.rs | 3 +- src-tauri/src/utils/mod.rs | 3 - 10 files changed, 181 insertions(+), 131 deletions(-) create mode 100644 src-tauri/crates/draft/Cargo.toml rename src-tauri/{benches/draft_benchmark.rs => crates/draft/bench/benche_me.rs} (90%) create mode 100644 src-tauri/crates/draft/src/lib.rs rename src-tauri/{src/utils/draft.rs => crates/draft/tests/test_me.rs} (71%) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 80e7c3a14..35cd86855 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1122,6 +1122,7 @@ dependencies = [ "criterion", "deelevate", "delay_timer", + "draft", "dunce", "flexi_logger", "futures", @@ -2016,6 +2017,16 @@ dependencies = [ "serde", ] +[[package]] +name = "draft" +version = "0.1.0" +dependencies = [ + "anyhow", + "criterion", + "parking_lot", + "tokio", +] + [[package]] name = "dtoa" version = "1.0.10" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e3bcc8516..67a142f6f 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,8 +17,11 @@ identifier = "io.github.clash-verge-rev.clash-verge-rev" tauri-build = { version = "2.5.2", features = [] } [dependencies] +parking_lot = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +draft = { workspace = true } warp = { version = "0.4.2", features = ["server"] } -anyhow = "1.0.100" open = "5.3.2" log = "0.4.28" dunce = "1.0.5" @@ -31,14 +34,7 @@ serde_yaml_ng = "0.10.0" once_cell = { version = "1.21.3", features = ["parking_lot"] } port_scanner = "0.1.5" delay_timer = "0.11.6" -parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] } percent-encoding = "2.3.2" -tokio = { version = "1.48.0", features = [ - "rt-multi-thread", - "macros", - "time", - "sync", -] } serde = { version = "1.0.228", features = ["derive"] } reqwest = { version = "0.12.24", features = ["json", "cookies"] } regex = "1.12.2" @@ -120,6 +116,28 @@ tauri-plugin-autostart = "2.5.1" tauri-plugin-global-shortcut = "2.3.1" tauri-plugin-updater = "2.9.0" +[dev-dependencies] +criterion = { workspace = true } + +[workspace.dependencies] +draft = { path = "crates/draft" } +parking_lot = { version = "0.12.5", features = [ + "hardware-lock-elision", + "send_guard", +] } +anyhow = "1.0.100" +criterion = { version = "0.7.0", features = ["async_tokio"] } +tokio = { version = "1.48.0", features = [ + "rt-multi-thread", + "macros", + "time", + "sync", +] } + +[workspace] +members = ["crates/*"] +resolver = "2" + [features] default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] @@ -129,11 +147,6 @@ tokio-trace = ["console-subscriber"] clippy = ["tauri/test"] tracing = [] -[[bench]] -name = "draft_benchmark" -path = "benches/draft_benchmark.rs" -harness = false - [profile.release] panic = "abort" codegen-units = 1 @@ -167,9 +180,6 @@ strip = false name = "app_lib" crate-type = ["staticlib", "cdylib", "rlib"] -[dev-dependencies] -criterion = { version = "0.7.0", features = ["async_tokio"] } - [lints.clippy] # Core categories - most important for code safety and correctness correctness = { level = "deny", priority = -1 } diff --git a/src-tauri/crates/draft/Cargo.toml b/src-tauri/crates/draft/Cargo.toml new file mode 100644 index 000000000..b0d51e2cc --- /dev/null +++ b/src-tauri/crates/draft/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "draft" +version = "0.1.0" +edition = "2024" + +[[bench]] +name = "draft_bench" +path = "bench/benche_me.rs" +harness = false + +[dependencies] +parking_lot = { workspace = true } +anyhow = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } +tokio = { workspace = true } diff --git a/src-tauri/benches/draft_benchmark.rs b/src-tauri/crates/draft/bench/benche_me.rs similarity index 90% rename from src-tauri/benches/draft_benchmark.rs rename to src-tauri/crates/draft/bench/benche_me.rs index c8a07c536..d5d8df7ea 100644 --- a/src-tauri/benches/draft_benchmark.rs +++ b/src-tauri/crates/draft/bench/benche_me.rs @@ -3,17 +3,30 @@ use std::hint::black_box; use std::process; use tokio::runtime::Runtime; -use app_lib::config::IVerge; -use app_lib::utils::Draft as DraftNew; +use draft::Draft; -/// 创建测试数据 -fn make_draft() -> DraftNew { +#[derive(Clone, Debug)] +struct IVerge { + enable_auto_launch: Option, + enable_tun_mode: Option, +} + +impl Default for IVerge { + fn default() -> Self { + Self { + enable_auto_launch: None, + enable_tun_mode: None, + } + } +} + +fn make_draft() -> Draft { let verge = IVerge { enable_auto_launch: Some(true), enable_tun_mode: Some(false), ..Default::default() }; - DraftNew::new(verge) + Draft::new(verge) } pub fn bench_draft(c: &mut Criterion) { diff --git a/src-tauri/crates/draft/src/lib.rs b/src-tauri/crates/draft/src/lib.rs new file mode 100644 index 000000000..5d548d091 --- /dev/null +++ b/src-tauri/crates/draft/src/lib.rs @@ -0,0 +1,102 @@ +use parking_lot::RwLock; +use std::sync::Arc; + +pub type SharedBox = Arc>; +type DraftInner = (SharedBox, Option>); + +/// Draft 管理:committed 与 optional draft 都以 Arc> 存储, +// (committed_snapshot, optional_draft_snapshot) +#[derive(Debug, Clone)] +pub struct Draft { + inner: Arc>>, +} + +impl Draft { + #[inline] + pub fn new(data: T) -> Self { + Self { + inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))), + } + } + /// 以 Arc> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc) + #[inline] + pub fn data_arc(&self) -> SharedBox { + let guard = self.inner.read(); + Arc::clone(&guard.0) + } + + /// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照 + /// 这也是零拷贝:只 clone Arc,不 clone T + #[inline] + pub fn latest_arc(&self) -> SharedBox { + let guard = self.inner.read(); + guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0)) + } + + /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T) + /// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T; + /// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。 + #[inline] + pub fn edit_draft(&self, f: F) -> R + where + F: FnOnce(&mut T) -> R, + { + // 先获得写锁以创建或取出草稿 Arc 的可变引用位置 + let mut guard = self.inner.write(); + let mut draft_arc = if guard.1.is_none() { + Arc::clone(&guard.0) + } else { + #[allow(clippy::unwrap_used)] + guard.1.take().unwrap() + }; + drop(guard); + // Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box(要求 T: Clone) + let boxed = Arc::make_mut(&mut draft_arc); // &mut Box + // 对 Box 解引用得到 &mut T + let result = f(&mut **boxed); + // 恢复修改后的草稿 Arc + self.inner.write().1 = Some(draft_arc); + result + } + + /// 将草稿提交到已提交位置(替换),并清除草稿 + #[inline] + pub fn apply(&self) { + let mut guard = self.inner.write(); + if let Some(d) = guard.1.take() { + guard.0 = d; + } + } + + /// 丢弃草稿(如果存在) + #[inline] + pub fn discard(&self) { + let mut guard = self.inner.write(); + guard.1 = None; + } + + /// 异步地以拥有 Box 的方式修改已提交数据:将克隆一次已提交数据到本地, + /// 异步闭包返回新的 Box(替换已提交数据)和业务返回值 R。 + #[inline] + pub async fn with_data_modify(&self, f: F) -> Result + where + T: Send + Sync + 'static, + F: FnOnce(Box) -> Fut + Send, + Fut: std::future::Future, R), anyhow::Error>> + Send, + { + // 读取已提交快照(cheap Arc clone, 然后得到 Box 所有权 via clone) + // 注意:为了让闭包接收 Box 所有权,我们需要 clone 底层 T(不可避免) + let local: Box = { + let guard = self.inner.read(); + // 将 Arc> 的 Box clone 出来(会调用 T: Clone) + (*guard.0).clone() + }; + + let (new_local, res) = f(local).await?; + + // 将新的 Box 放到已提交位置(包进 Arc) + self.inner.write().0 = Arc::new(new_local); + + Ok(res) + } +} diff --git a/src-tauri/src/utils/draft.rs b/src-tauri/crates/draft/tests/test_me.rs similarity index 71% rename from src-tauri/src/utils/draft.rs rename to src-tauri/crates/draft/tests/test_me.rs index 07782f4bd..448fc7945 100644 --- a/src-tauri/src/utils/draft.rs +++ b/src-tauri/crates/draft/tests/test_me.rs @@ -1,110 +1,7 @@ -use parking_lot::RwLock; -use std::sync::Arc; - -pub type SharedBox = Arc>; -type DraftInner = (SharedBox, Option>); - -/// Draft 管理:committed 与 optional draft 都以 Arc> 存储, -// (committed_snapshot, optional_draft_snapshot) -#[derive(Debug, Clone)] -pub struct Draft { - inner: Arc>>, -} - -impl Draft { - #[inline] - pub fn new(data: T) -> Self { - Self { - inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))), - } - } - /// 以 Arc> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc) - #[inline] - pub fn data_arc(&self) -> SharedBox { - let guard = self.inner.read(); - Arc::clone(&guard.0) - } - - /// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照 - /// 这也是零拷贝:只 clone Arc,不 clone T - #[inline] - pub fn latest_arc(&self) -> SharedBox { - let guard = self.inner.read(); - guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0)) - } - - /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T) - /// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T; - /// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。 - #[inline] - pub fn edit_draft(&self, f: F) -> R - where - F: FnOnce(&mut T) -> R, - { - // 先获得写锁以创建或取出草稿 Arc 的可变引用位置 - let mut guard = self.inner.write(); - let mut draft_arc = if guard.1.is_none() { - Arc::clone(&guard.0) - } else { - #[allow(clippy::unwrap_used)] - guard.1.take().unwrap() - }; - drop(guard); - // Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box(要求 T: Clone) - let boxed = Arc::make_mut(&mut draft_arc); // &mut Box - // 对 Box 解引用得到 &mut T - let result = f(&mut **boxed); - // 恢复修改后的草稿 Arc - self.inner.write().1 = Some(draft_arc); - result - } - - /// 将草稿提交到已提交位置(替换),并清除草稿 - #[inline] - pub fn apply(&self) { - let mut guard = self.inner.write(); - if let Some(d) = guard.1.take() { - guard.0 = d; - } - } - - /// 丢弃草稿(如果存在) - #[inline] - pub fn discard(&self) { - let mut guard = self.inner.write(); - guard.1 = None; - } - - /// 异步地以拥有 Box 的方式修改已提交数据:将克隆一次已提交数据到本地, - /// 异步闭包返回新的 Box(替换已提交数据)和业务返回值 R。 - #[inline] - pub async fn with_data_modify(&self, f: F) -> Result - where - T: Send + Sync + 'static, - F: FnOnce(Box) -> Fut + Send, - Fut: std::future::Future, R), anyhow::Error>> + Send, - { - // 读取已提交快照(cheap Arc clone, 然后得到 Box 所有权 via clone) - // 注意:为了让闭包接收 Box 所有权,我们需要 clone 底层 T(不可避免) - let local: Box = { - let guard = self.inner.read(); - // 将 Arc> 的 Box clone 出来(会调用 T: Clone) - (*guard.0).clone() - }; - - let (new_local, res) = f(local).await?; - - // 将新的 Box 放到已提交位置(包进 Arc) - self.inner.write().0 = Arc::new(new_local); - - Ok(res) - } -} - #[cfg(test)] mod tests { - use super::*; use anyhow::anyhow; + use draft::Draft; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; diff --git a/src-tauri/src/cmd/verge.rs b/src-tauri/src/cmd/verge.rs index 943eb9db6..b58e9e524 100644 --- a/src-tauri/src/cmd/verge.rs +++ b/src-tauri/src/cmd/verge.rs @@ -1,5 +1,6 @@ use super::CmdResult; -use crate::{cmd::StringifyErr as _, config::IVerge, feat, utils::draft::SharedBox}; +use crate::{cmd::StringifyErr as _, config::IVerge, feat}; +use draft::SharedBox; /// 获取Verge配置 #[tauri::command] diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index 25a454720..316d99c50 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -5,10 +5,11 @@ use crate::{ constants::{files, timing}, core::{CoreManager, handle, service, tray, validate::CoreConfigValidator}, enhance, logging, logging_error, - utils::{Draft, dirs, help, logging::Type}, + utils::{dirs, help, logging::Type}, }; use anyhow::{Result, anyhow}; use backoff::{Error as BackoffError, ExponentialBackoff}; +use draft::Draft; use smartstring::alias::String; use std::path::PathBuf; use tokio::sync::OnceCell; diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index a127a5bb1..d9932f79d 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -3,9 +3,10 @@ use crate::{ core::{CoreManager, handle, hotkey, sysopt, tray}, logging, logging_error, module::{auto_backup::AutoBackupManager, lightweight}, - utils::{draft::SharedBox, logging::Type}, + utils::logging::Type, }; use anyhow::Result; +use draft::SharedBox; use serde_yaml_ng::Mapping; /// Patch Clash configuration diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 7c3e1abac..c9c905443 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,6 +1,5 @@ pub mod autostart; pub mod dirs; -pub mod draft; pub mod format; pub mod help; pub mod i18n; @@ -15,5 +14,3 @@ pub mod server; pub mod singleton; pub mod tmpl; pub mod window_manager; - -pub use draft::Draft;