perf(enhance): cut fixed config-generation cost

Every settings toggle reparsed the profile and chain files, ran four
Boa interpreters, serialized the runtime config twice, and re-ran
mihomo -t over bytes the core had just accepted. Builtin and template
scripts now run without Boa, custom scripts bind the config through
JSON.parse and take ownership instead of per-script clones, the config
serializes once, and mihomo -t is skipped unless the bytes, core
build, or geo assets changed. Heavy parses and serializations moved
to spawn_blocking. Large-subscription toggles drop from hundreds of
milliseconds to ~140ms with no new retained state.
This commit is contained in:
Tunglies
2026-09-10 06:32:51 +08:00
parent 92624108c9
commit 8812923eda
13 changed files with 269 additions and 239 deletions
+7 -3
View File
@@ -1,7 +1,7 @@
use super::{CoreManager, PROFILE_SELECTIONS_PENDING_COMMIT, RunningMode};
use crate::core::service::StageRequest;
use crate::{
config::{Config, ConfigType, IProfiles, runtime::IRuntime},
config::{Config, IProfiles, runtime::IRuntime},
constants::timing,
core::{
handle,
@@ -299,12 +299,16 @@ impl CoreManager {
/// Validates and applies the caller's transaction, committing only on success.
async fn validate_and_apply(&self, transaction: DraftTransaction<'_>) -> Result<ValidationOutcome> {
let outcome = CoreConfigValidator::global().validate_config_outcome().await?;
// One serialization feeds check and run files; the core never applies unvalidated bytes.
let yaml = Config::runtime_config_yaml().await?;
let outcome = CoreConfigValidator::global()
.validate_config_outcome_with(&yaml)
.await?;
if !outcome.is_valid() {
return Ok(outcome);
}
let run_path = Config::generate_file(ConfigType::Run).await?;
let run_path = Config::write_runtime_file(&yaml).await?;
self.apply_config(run_path).await?;
transaction.commit();
Ok(ValidationOutcome::Valid)
+2 -2
View File
@@ -104,7 +104,7 @@ impl CoreManager {
handle::Handle::app_handle()
.mihomo()
.update_socket_path(dirs::path_to_str(&sidecar_ipc)?.to_owned())?;
let config_file = Config::generate_file(crate::config::ConfigType::Run).await?;
let config_file = Config::generate_file().await?;
let app_handle = handle::Handle::app_handle();
let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?;
@@ -255,7 +255,7 @@ impl CoreManager {
pub(super) async fn start_core_by_service(&self) -> Result<()> {
self.core_starting();
let service_ipc = dirs::ipc_path()?;
let config_file = Config::generate_file(crate::config::ConfigType::Run).await?;
let config_file = Config::generate_file().await?;
handle::Handle::app_handle()
.mihomo()
.update_socket_path(dirs::path_to_str(&service_ipc)?.to_owned())?;
+1 -1
View File
@@ -5,7 +5,7 @@ use serde_yaml_ng::Value;
use std::collections::HashSet;
use std::path::{Component, Path, PathBuf};
const GEO_ASSETS: &[&str] = &[
pub(crate) const GEO_ASSETS: &[&str] = &[
"Country.mmdb",
"geoip.dat",
"geosite.dat",
+71 -22
View File
@@ -4,17 +4,49 @@ use serde::Serialize;
use smartstring::alias::String;
use std::{
fmt,
hash::{DefaultHasher, Hash as _, Hasher as _},
sync::LazyLock,
sync::atomic::{AtomicBool, Ordering},
};
use tauri_plugin_shell::ShellExt as _;
use tokio::fs;
use crate::config::{Config, ConfigType};
use crate::config::Config;
use crate::core::handle;
use crate::singleton;
use crate::utils::dirs;
use crate::utils::{dirs, help};
use clash_verge_logging::{Type, logging};
const SYNTAX_CHECK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
static LAST_VALIDATED: LazyLock<parking_lot::Mutex<Option<u64>>> = LazyLock::new(|| parking_lot::Mutex::new(None));
async fn validation_fingerprint(yaml: &str) -> u64 {
let core = Config::verge().await.latest_arc().get_valid_clash_core();
let mut hasher = DefaultHasher::new();
yaml.hash(&mut hasher);
core.as_str().hash(&mut hasher);
let binary = std::env::current_exe()
.into_iter()
.map(|exe| exe.with_file_name(format!("{}{}", core.as_str(), std::env::consts::EXE_SUFFIX)));
// The test run reads geo databases from the app dir; a geo update must revalidate.
let geo = dirs::app_home_dir().into_iter().flat_map(|dir| {
super::runtime_bundle::GEO_ASSETS
.iter()
.map(move |asset| dir.join(asset))
});
for path in binary.chain(geo) {
match std::fs::metadata(&path) {
Ok(meta) => {
meta.len().hash(&mut hasher);
meta.modified().ok().hash(&mut hasher);
}
Err(_) => 0u8.hash(&mut hasher),
}
}
hasher.finish()
}
pub struct CoreConfigValidator {
is_processing: AtomicBool,
}
@@ -223,28 +255,33 @@ impl CoreConfigValidator {
};
logging!(debug, Type::Validate, "验证脚本文件: {}", path);
let has_main =
content.contains("function main") || content.contains("const main") || content.contains("let main");
use boa_engine::{Context, Source};
// Boa parsing is pure CPU; keep it off the async worker, with a timeout.
let syntax = crate::process::AsyncHandler::spawn_blocking(move || {
use boa_engine::{Context, Source};
let mut context = Context::default();
let _ = context.eval(Source::from_bytes(
"var console = Object.freeze({
log(...data){},
info(...data){},
error(...data){},
debug(...data){},
});",
));
let result = context.eval(Source::from_bytes(&content));
let mut context = Context::default();
let _ = context.eval(Source::from_bytes(
"var console = Object.freeze({log(...data){},info(...data){},error(...data){},debug(...data){}});",
));
context
.eval(Source::from_bytes(&content))
.map(|_| ())
.map_err(|err| err.to_string())
});
let result = match tokio::time::timeout(SYNTAX_CHECK_TIMEOUT, syntax).await {
Ok(Ok(evaluated)) => evaluated,
Ok(Err(join_err)) => Err(format!("syntax check task failed: {join_err}")),
Err(_) => Err("syntax check timed out".to_owned()),
};
match result {
Ok(_) => {
Ok(()) => {
logging!(debug, Type::Validate, "脚本语法验证通过: {}", path);
if !content.contains("function main")
&& !content.contains("const main")
&& !content.contains("let main")
{
if !has_main {
let error_msg = "Script must contain a main function";
logging!(warn, Type::Validate, "脚本缺少main函数: {}", path);
return Ok(ValidationOutcome::invalid_from_message(error_msg));
@@ -371,7 +408,8 @@ impl CoreConfigValidator {
}
}
pub async fn validate_config_outcome(&self) -> Result<ValidationOutcome> {
/// Skips the subprocess when these bytes were already accepted by the same core build.
pub async fn validate_config_outcome_with(&self, yaml: &str) -> Result<ValidationOutcome> {
if !self.try_start() {
logging!(info, Type::Validate, "验证已在进行中,跳过新的验证请求");
return Ok(ValidationOutcome::Busy);
@@ -381,9 +419,20 @@ impl CoreConfigValidator {
}
logging!(info, Type::Validate, "生成临时配置文件用于验证");
let config_path = Config::generate_file(ConfigType::Check).await?;
let config_path = dirs::path_to_str(&config_path)?;
Self::validate_config_internal_outcome(config_path).await
let fingerprint = validation_fingerprint(yaml).await;
if *LAST_VALIDATED.lock() == Some(fingerprint) {
logging!(info, Type::Validate, "配置与上次通过验证的字节一致,跳过内核检查");
return Ok(ValidationOutcome::Valid);
}
let check_path = dirs::app_home_dir()?.join(crate::constants::files::CHECK_CONFIG);
help::save_yaml_str(&check_path, yaml).await?;
let outcome = Self::validate_config_internal_outcome(dirs::path_to_str(&check_path)?).await?;
if outcome.is_valid() {
*LAST_VALIDATED.lock() = Some(fingerprint);
let _ = fs::remove_file(&check_path).await;
}
Ok(outcome)
}
}