From 8c4e3ee0c5dd2fdd4b5b4e0c5323d041afb86e21 Mon Sep 17 00:00:00 2001 From: Wood Date: Tue, 17 Mar 2026 17:19:04 +0800 Subject: [PATCH] Release v3.6.5: SWQOS core affinity and recommended sender thread indices - Add TradeConfig::with_swqos_cores_from_end(bool) to use last N CPU cores for SWQOS, reducing contention with main thread and default tokio workers. - Add recommended_sender_thread_core_indices(swqos_count) to get the same last-N core indices for with_dedicated_sender_threads (recommended combo for lower latency). - Document core affinity and latency in async_executor and with_dedicated_sender_threads. Made-with: Cursor --- Cargo.toml | 2 +- src/common/types.rs | 24 +++++++++++++++++++--- src/lib.rs | 32 ++++++++++++++++++++++++++++-- src/trading/core/async_executor.rs | 5 +++++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 096fd9b..9b15e0c 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "3.6.4" +version = "3.6.5" edition = "2021" authors = [ "William ", diff --git a/src/common/types.rs b/src/common/types.rs index 6a1c750..6c51a62 100755 --- a/src/common/types.rs +++ b/src/common/types.rs @@ -9,6 +9,8 @@ pub struct InfrastructureConfig { pub rpc_url: String, pub swqos_configs: Vec, pub commitment: CommitmentConfig, + /// When true, SWQOS sender threads use the *last* N cores instead of the first N. Reduces contention with main thread / default tokio workers that often use low-numbered cores. Default false. + pub swqos_cores_from_end: bool, } impl InfrastructureConfig { @@ -17,7 +19,12 @@ impl InfrastructureConfig { swqos_configs: Vec, commitment: CommitmentConfig, ) -> Self { - Self { rpc_url, swqos_configs, commitment } + Self { + rpc_url, + swqos_configs, + commitment, + swqos_cores_from_end: false, + } } /// Create from TradeConfig (extract infrastructure-only settings) @@ -26,6 +33,7 @@ impl InfrastructureConfig { rpc_url: config.rpc_url.clone(), swqos_configs: config.swqos_configs.clone(), commitment: config.commitment.clone(), + swqos_cores_from_end: config.swqos_cores_from_end, } } @@ -43,8 +51,8 @@ impl Hash for InfrastructureConfig { fn hash(&self, state: &mut H) { self.rpc_url.hash(state); self.swqos_configs.hash(state); - // Hash commitment level as string since CommitmentConfig doesn't impl Hash format!("{:?}", self.commitment).hash(state); + self.swqos_cores_from_end.hash(state); } } @@ -53,6 +61,7 @@ impl PartialEq for InfrastructureConfig { self.rpc_url == other.rpc_url && self.swqos_configs == other.swqos_configs && self.commitment == other.commitment + && self.swqos_cores_from_end == other.swqos_cores_from_end } } @@ -72,6 +81,8 @@ pub struct TradeConfig { pub log_enabled: bool, /// Whether to check minimum tip per SWQOS provider (filter out configs below min). Default false to save latency. pub check_min_tip: bool, + /// When true, SWQOS uses the *last* N cores (instead of the first N). Use when main thread / tokio use low-numbered cores to reduce CPU contention. Default false. + pub swqos_cores_from_end: bool, } impl TradeConfig { @@ -91,7 +102,8 @@ impl TradeConfig { create_wsol_ata_on_startup: true, // default: check and create on startup use_seed_optimize: true, // default: use seed optimization log_enabled: true, // default: enable all SDK logs - check_min_tip: false, // default: skip min tip check to reduce latency + check_min_tip: false, // default: skip min tip check to reduce latency + swqos_cores_from_end: false, } } @@ -111,6 +123,12 @@ impl TradeConfig { self.check_min_tip = check_min_tip; self } + + /// Use the *last* N cores for SWQOS (instead of the first N). Call this when the main thread or tokio workers use low-numbered cores to avoid binding SWQOS to busy cores. Default false. + pub fn with_swqos_cores_from_end(mut self, from_end: bool) -> Self { + self.swqos_cores_from_end = from_end; + self + } } pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient; diff --git a/src/lib.rs b/src/lib.rs index f51f5a6..c8a87e2 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -185,7 +185,15 @@ impl TradingInfrastructure { let max_by_cores = (num_cores * 2 / 3).max(1); let cap = swqos_count.min(max_by_cores).max(1); let ids = core_affinity::get_core_ids() - .map(|all| all.into_iter().take(cap).collect::>()) + .map(|all| { + let v: Vec<_> = all.into_iter().collect(); + let len = v.len(); + if config.swqos_cores_from_end && len >= cap { + v.into_iter().skip(len - cap).collect() + } else { + v.into_iter().take(cap).collect() + } + }) .unwrap_or_default(); (cap, Arc::new(ids)) }; @@ -200,6 +208,23 @@ impl TradingInfrastructure { } } +/// When using `TradeConfig::with_swqos_cores_from_end(true)`, returns the same "last N" core indices +/// that the infrastructure uses. Pass the result to `TradingClient::with_dedicated_sender_threads` +/// for 方式 C (组合使用): SWQOS on last N cores and dedicated sender threads pinned to those cores. +/// +/// Returns `None` if core count cannot be determined. `swqos_count` is typically `swqos_configs.len()`. +pub fn recommended_sender_thread_core_indices(swqos_count: usize) -> Option> { + let all = core_affinity::get_core_ids()?; + let num_cores = all.len(); + if num_cores == 0 { + return None; + } + let max_by_cores = (num_cores * 2 / 3).max(1); + let cap = swqos_count.min(max_by_cores).max(1).min(num_cores); + let start = num_cores.saturating_sub(cap); + Some((start..num_cores).collect()) +} + /// Main trading client for Solana DeFi protocols /// /// `SolTradingSDK` provides a unified interface for trading across multiple Solana DEXs @@ -635,7 +660,10 @@ impl TradingClient { /// Concurrency and core count are capped internally (≤ swqos count, ≤ 2/3 of CPU cores). /// - `None`: keep default (shared tokio pool). /// - `Some(vec![])`: dedicated threads with default count, no core pinning. - /// - `Some(indices)`: dedicated threads pinned to those core indices (trimmed to cap). + /// - `Some(indices)`: dedicated threads pinned to those core indices (trimmed to cap). + /// + /// **Latency note:** If a core is busy with other work (node, bot), SWQOS submit on that core can be delayed. + /// For lowest latency, pass core indices that are *reserved* for SWQOS (do not run other CPU-heavy work on those cores). pub fn with_dedicated_sender_threads(mut self, core_indices: Option>) -> Self { match core_indices { None => { diff --git a/src/trading/core/async_executor.rs b/src/trading/core/async_executor.rs index 46c97cc..69c8714 100644 --- a/src/trading/core/async_executor.rs +++ b/src/trading/core/async_executor.rs @@ -5,6 +5,11 @@ //! - **Dedicated threads** (opt-in via `with_dedicated_sender_threads`): N OS threads run sender work only, optionally pinned to cores. //! - **Arc**: Shared data behind `Arc` → clone = refcount increment (no data copy). //! - **Refs**: `build_transaction` takes refs only; worker path avoids extra clones. +//! +//! **Core affinity & latency:** Each job is assigned a core (round-robin from `effective_core_ids`). When a worker runs a job, +//! it sets thread affinity to that core. If that core is busy with other work (e.g. node sync, bot logic), SWQOS submit on that +//! core will compete for CPU and latency can increase. For lowest latency, reserve a subset of cores for SWQOS only via +//! `with_dedicated_sender_threads(Some(indices))` and avoid running other CPU-heavy work on those core indices. use anyhow::{anyhow, Result}; use crossbeam_queue::ArrayQueue;