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<RwLock>` 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.
This commit is contained in:
Tunglies
2025-11-16 00:33:21 +08:00
committed by GitHub
parent e66b8e7894
commit dbb4877be6
10 changed files with 181 additions and 131 deletions

11
src-tauri/Cargo.lock generated
View File

@@ -1122,6 +1122,7 @@ dependencies = [
"criterion", "criterion",
"deelevate", "deelevate",
"delay_timer", "delay_timer",
"draft",
"dunce", "dunce",
"flexi_logger", "flexi_logger",
"futures", "futures",
@@ -2016,6 +2017,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "draft"
version = "0.1.0"
dependencies = [
"anyhow",
"criterion",
"parking_lot",
"tokio",
]
[[package]] [[package]]
name = "dtoa" name = "dtoa"
version = "1.0.10" version = "1.0.10"

View File

@@ -17,8 +17,11 @@ identifier = "io.github.clash-verge-rev.clash-verge-rev"
tauri-build = { version = "2.5.2", features = [] } tauri-build = { version = "2.5.2", features = [] }
[dependencies] [dependencies]
parking_lot = { workspace = true }
anyhow = { workspace = true }
tokio = { workspace = true }
draft = { workspace = true }
warp = { version = "0.4.2", features = ["server"] } warp = { version = "0.4.2", features = ["server"] }
anyhow = "1.0.100"
open = "5.3.2" open = "5.3.2"
log = "0.4.28" log = "0.4.28"
dunce = "1.0.5" dunce = "1.0.5"
@@ -31,14 +34,7 @@ serde_yaml_ng = "0.10.0"
once_cell = { version = "1.21.3", features = ["parking_lot"] } once_cell = { version = "1.21.3", features = ["parking_lot"] }
port_scanner = "0.1.5" port_scanner = "0.1.5"
delay_timer = "0.11.6" delay_timer = "0.11.6"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
percent-encoding = "2.3.2" percent-encoding = "2.3.2"
tokio = { version = "1.48.0", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
reqwest = { version = "0.12.24", features = ["json", "cookies"] } reqwest = { version = "0.12.24", features = ["json", "cookies"] }
regex = "1.12.2" regex = "1.12.2"
@@ -120,6 +116,28 @@ tauri-plugin-autostart = "2.5.1"
tauri-plugin-global-shortcut = "2.3.1" tauri-plugin-global-shortcut = "2.3.1"
tauri-plugin-updater = "2.9.0" 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] [features]
default = ["custom-protocol"] default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
@@ -129,11 +147,6 @@ tokio-trace = ["console-subscriber"]
clippy = ["tauri/test"] clippy = ["tauri/test"]
tracing = [] tracing = []
[[bench]]
name = "draft_benchmark"
path = "benches/draft_benchmark.rs"
harness = false
[profile.release] [profile.release]
panic = "abort" panic = "abort"
codegen-units = 1 codegen-units = 1
@@ -167,9 +180,6 @@ strip = false
name = "app_lib" name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[dev-dependencies]
criterion = { version = "0.7.0", features = ["async_tokio"] }
[lints.clippy] [lints.clippy]
# Core categories - most important for code safety and correctness # Core categories - most important for code safety and correctness
correctness = { level = "deny", priority = -1 } correctness = { level = "deny", priority = -1 }

View File

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

View File

@@ -3,17 +3,30 @@ use std::hint::black_box;
use std::process; use std::process;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use app_lib::config::IVerge; use draft::Draft;
use app_lib::utils::Draft as DraftNew;
/// 创建测试数据 #[derive(Clone, Debug)]
fn make_draft() -> DraftNew<IVerge> { struct IVerge {
enable_auto_launch: Option<bool>,
enable_tun_mode: Option<bool>,
}
impl Default for IVerge {
fn default() -> Self {
Self {
enable_auto_launch: None,
enable_tun_mode: None,
}
}
}
fn make_draft() -> Draft<IVerge> {
let verge = IVerge { let verge = IVerge {
enable_auto_launch: Some(true), enable_auto_launch: Some(true),
enable_tun_mode: Some(false), enable_tun_mode: Some(false),
..Default::default() ..Default::default()
}; };
DraftNew::new(verge) Draft::new(verge)
} }
pub fn bench_draft(c: &mut Criterion) { pub fn bench_draft(c: &mut Criterion) {

View File

@@ -0,0 +1,102 @@
use parking_lot::RwLock;
use std::sync::Arc;
pub type SharedBox<T> = Arc<Box<T>>;
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
/// Draft 管理committed 与 optional draft 都以 Arc<Box<T>> 存储,
// (committed_snapshot, optional_draft_snapshot)
#[derive(Debug, Clone)]
pub struct Draft<T: Clone> {
inner: Arc<RwLock<DraftInner<T>>>,
}
impl<T: Clone> Draft<T> {
#[inline]
pub fn new(data: T) -> Self {
Self {
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
}
}
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc
#[inline]
pub fn data_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
Arc::clone(&guard.0)
}
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
/// 这也是零拷贝:只 clone Arc不 clone T
#[inline]
pub fn latest_arc(&self) -> SharedBox<T> {
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<F, R>(&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>(要求 T: Clone
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
// 对 Box<T> 解引用得到 &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<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
#[inline]
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
where
T: Send + Sync + 'static,
F: FnOnce(Box<T>) -> Fut + Send,
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
{
// 读取已提交快照cheap Arc clone, 然后得到 Box<T> 所有权 via clone
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T不可避免
let local: Box<T> = {
let guard = self.inner.read();
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone
(*guard.0).clone()
};
let (new_local, res) = f(local).await?;
// 将新的 Box<T> 放到已提交位置(包进 Arc
self.inner.write().0 = Arc::new(new_local);
Ok(res)
}
}

View File

@@ -1,110 +1,7 @@
use parking_lot::RwLock;
use std::sync::Arc;
pub type SharedBox<T> = Arc<Box<T>>;
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
/// Draft 管理committed 与 optional draft 都以 Arc<Box<T>> 存储,
// (committed_snapshot, optional_draft_snapshot)
#[derive(Debug, Clone)]
pub struct Draft<T: Clone> {
inner: Arc<RwLock<DraftInner<T>>>,
}
impl<T: Clone> Draft<T> {
#[inline]
pub fn new(data: T) -> Self {
Self {
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
}
}
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc
#[inline]
pub fn data_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
Arc::clone(&guard.0)
}
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
/// 这也是零拷贝:只 clone Arc不 clone T
#[inline]
pub fn latest_arc(&self) -> SharedBox<T> {
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<F, R>(&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>(要求 T: Clone
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
// 对 Box<T> 解引用得到 &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<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
#[inline]
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
where
T: Send + Sync + 'static,
F: FnOnce(Box<T>) -> Fut + Send,
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
{
// 读取已提交快照cheap Arc clone, 然后得到 Box<T> 所有权 via clone
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T不可避免
let local: Box<T> = {
let guard = self.inner.read();
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone
(*guard.0).clone()
};
let (new_local, res) = f(local).await?;
// 将新的 Box<T> 放到已提交位置(包进 Arc
self.inner.write().0 = Arc::new(new_local);
Ok(res)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use anyhow::anyhow; use anyhow::anyhow;
use draft::Draft;
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

View File

@@ -1,5 +1,6 @@
use super::CmdResult; 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配置 /// 获取Verge配置
#[tauri::command] #[tauri::command]

View File

@@ -5,10 +5,11 @@ use crate::{
constants::{files, timing}, constants::{files, timing},
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator}, core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
enhance, logging, logging_error, enhance, logging, logging_error,
utils::{Draft, dirs, help, logging::Type}, utils::{dirs, help, logging::Type},
}; };
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use backoff::{Error as BackoffError, ExponentialBackoff}; use backoff::{Error as BackoffError, ExponentialBackoff};
use draft::Draft;
use smartstring::alias::String; use smartstring::alias::String;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;

View File

@@ -3,9 +3,10 @@ use crate::{
core::{CoreManager, handle, hotkey, sysopt, tray}, core::{CoreManager, handle, hotkey, sysopt, tray},
logging, logging_error, logging, logging_error,
module::{auto_backup::AutoBackupManager, lightweight}, module::{auto_backup::AutoBackupManager, lightweight},
utils::{draft::SharedBox, logging::Type}, utils::logging::Type,
}; };
use anyhow::Result; use anyhow::Result;
use draft::SharedBox;
use serde_yaml_ng::Mapping; use serde_yaml_ng::Mapping;
/// Patch Clash configuration /// Patch Clash configuration

View File

@@ -1,6 +1,5 @@
pub mod autostart; pub mod autostart;
pub mod dirs; pub mod dirs;
pub mod draft;
pub mod format; pub mod format;
pub mod help; pub mod help;
pub mod i18n; pub mod i18n;
@@ -15,5 +14,3 @@ pub mod server;
pub mod singleton; pub mod singleton;
pub mod tmpl; pub mod tmpl;
pub mod window_manager; pub mod window_manager;
pub use draft::Draft;