diff --git a/src/LibreQoS.py b/src/LibreQoS.py index d44e4968..8791d1a9 100755 --- a/src/LibreQoS.py +++ b/src/LibreQoS.py @@ -35,6 +35,7 @@ from liblqos_python import is_lqosd_alive, clear_ip_mappings, delete_ip_mapping, on_a_stick, get_tree_weights, get_weights, is_network_flat, get_libreqos_directory, enable_insight_topology, \ is_insight_enabled, \ fast_queues_fq_codel, \ + shaping_cpu_count, \ Bakery # Optional: urgent issue submission (available in newer liblqos_python) @@ -189,7 +190,10 @@ def findQueuesAvailable(interfaceName): else: queuesAvailable = queues_available_override() print(f"Interface {interfaceName} NIC queues (Override):\t\t\t" + str(queuesAvailable)) - cpuCount = multiprocessing.cpu_count() + try: + cpuCount = shaping_cpu_count() + except Exception: + cpuCount = multiprocessing.cpu_count() print("CPU cores:\t\t\t" + str(cpuCount)) if queuesAvailable < 2: raise SystemError(f'Only 1 NIC rx/tx queue available for interface {interfaceName}. You will need to use a NIC with 2 or more rx/tx queues available.') diff --git a/src/rust/lqos_config/src/cpu_topology.rs b/src/rust/lqos_config/src/cpu_topology.rs new file mode 100644 index 00000000..d74f6a90 --- /dev/null +++ b/src/rust/lqos_config/src/cpu_topology.rs @@ -0,0 +1,248 @@ +//! CPU topology helpers (hybrid P-core/E-core detection). +//! +//! LibreQoS primarily runs on Linux and may be deployed on hybrid CPUs that +//! include both performance cores ("P-cores") and efficiency cores ("E-cores"). +//! When possible, we detect E-cores via sysfs and provide a list of CPUs that +//! should be used for shaping / CPU binning. + +use crate::Config; +use std::collections::HashSet; +use std::path::Path; +use thiserror::Error; + +const POSSIBLE_CPUS_PATH: &str = "/sys/devices/system/cpu/possible"; + +const CPU_CORE_SYSFS_PATHS: [&str; 2] = [ + "/sys/bus/event_source/devices/cpu_core/cpus", + "/sys/devices/cpu_core/cpus", +]; + +const CPU_ATOM_SYSFS_PATHS: [&str; 2] = [ + "/sys/bus/event_source/devices/cpu_atom/cpus", + "/sys/devices/cpu_atom/cpus", +]; + +/// Indicates which mechanism was used to choose shaping CPUs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShapingCpuSource { + /// Exclusion is disabled; use all possible CPUs. + ExclusionDisabled, + /// Used `/sys/.../cpu_core/cpus` to identify P-cores. + CpuCoreSysfs, + /// Used `/sys/.../cpu_atom/cpus` to identify E-cores, then computed `possible - atom`. + PossibleMinusAtomSysfs, + /// Hybrid sysfs paths were unavailable; used `/sys/devices/system/cpu/possible`. + FallbackAllPossible, + /// Could not read `/sys/devices/system/cpu/possible`; used Rust's available parallelism. + FallbackAvailableParallelism, +} + +/// Result of hybrid CPU detection. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ShapingCpuDetection { + /// Whether efficiency core exclusion was requested. + pub exclude_efficiency_cores: bool, + /// Source used to determine the shaping CPU list. + pub source: ShapingCpuSource, + /// All possible CPU IDs (best effort). + pub possible: Vec, + /// Detected performance-core CPU IDs (may be empty if unknown). + pub performance: Vec, + /// Detected efficiency-core CPU IDs (may be empty if unknown). + pub efficiency: Vec, + /// Final CPU list that should be used for shaping/bins. + pub shaping: Vec, +} + +/// Errors that can occur when parsing a Linux cpulist string. +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum CpuListParseError { + /// Input was empty or contained no CPU entries. + #[error("CPU list is empty")] + Empty, + /// A numeric component failed to parse. + #[error("Unable to parse number in CPU list")] + ParseNumber, + /// A range was malformed (e.g. `3-1`). + #[error("Invalid range in CPU list")] + InvalidRange, +} + +fn parse_cpu_list(input: &str) -> Result, CpuListParseError> { + let trimmed = input.trim(); + if trimmed.is_empty() { + return Err(CpuListParseError::Empty); + } + let mut out = Vec::new(); + for part in trimmed.split(',') { + let p = part.trim(); + if p.is_empty() { + continue; + } + if let Some((start_s, end_s)) = p.split_once('-') { + let start = start_s.trim().parse::().map_err(|_| CpuListParseError::ParseNumber)?; + let end = end_s.trim().parse::().map_err(|_| CpuListParseError::ParseNumber)?; + if end < start { + return Err(CpuListParseError::InvalidRange); + } + for cpu in start..=end { + out.push(cpu); + } + } else { + let cpu = p.parse::().map_err(|_| CpuListParseError::ParseNumber)?; + out.push(cpu); + } + } + if out.is_empty() { + return Err(CpuListParseError::Empty); + } + out.sort_unstable(); + out.dedup(); + Ok(out) +} + +fn try_read_cpu_list(path: &str) -> Option> { + if !Path::new(path).exists() { + return None; + } + let raw = std::fs::read_to_string(path).ok()?; + let list = parse_cpu_list(&raw).ok()?; + (!list.is_empty()).then_some(list) +} + +fn first_cpu_list(paths: &[&str]) -> Option> { + for p in paths.iter().copied() { + if let Some(list) = try_read_cpu_list(p) { + return Some(list); + } + } + None +} + +fn possible_cpu_list() -> (Vec, ShapingCpuSource) { + if let Some(list) = try_read_cpu_list(POSSIBLE_CPUS_PATH) { + return (list, ShapingCpuSource::FallbackAllPossible); + } + + let fallback = std::thread::available_parallelism() + .map(|n| (0..(n.get() as u32)).collect::>()) + .unwrap_or_else(|_| vec![0]); + (fallback, ShapingCpuSource::FallbackAvailableParallelism) +} + +/// Detects the set of CPUs that should be used for shaping/CPU binning. +/// +/// When `cfg.exclude_efficiency_cores` is enabled, this attempts to identify +/// performance cores via sysfs. If detection fails or yields an empty set, +/// this falls back to "all possible CPUs". +pub fn detect_shaping_cpus(cfg: &Config) -> ShapingCpuDetection { + let exclude = cfg.exclude_efficiency_cores; + + let (possible, possible_source) = possible_cpu_list(); + + if !exclude { + return ShapingCpuDetection { + exclude_efficiency_cores: exclude, + source: ShapingCpuSource::ExclusionDisabled, + possible: possible.clone(), + performance: possible.clone(), + efficiency: Vec::new(), + shaping: possible, + }; + } + + let perf_opt = first_cpu_list(&CPU_CORE_SYSFS_PATHS); + let eff_opt = first_cpu_list(&CPU_ATOM_SYSFS_PATHS); + + // Prefer cpu_core list if present. + if let Some(mut perf) = perf_opt.clone() { + // If both are present, remove overlaps just in case. + if let Some(eff) = eff_opt.clone() { + if !eff.is_empty() && !perf.is_empty() { + let eff_set: HashSet = eff.iter().copied().collect(); + perf.retain(|c| !eff_set.contains(c)); + } + } + + // Ensure it is a subset of possible CPUs, if that list exists. + if !possible.is_empty() { + let poss_set: HashSet = possible.iter().copied().collect(); + perf.retain(|c| poss_set.contains(c)); + } + + if !perf.is_empty() { + return ShapingCpuDetection { + exclude_efficiency_cores: exclude, + source: ShapingCpuSource::CpuCoreSysfs, + possible, + performance: perf.clone(), + efficiency: eff_opt.unwrap_or_default(), + shaping: perf, + }; + } + } + + // Next-best: if we know efficiency CPUs, compute possible - efficiency. + if let Some(eff) = eff_opt.clone() { + if !eff.is_empty() && !possible.is_empty() { + let eff_set: HashSet = eff.iter().copied().collect(); + let perf: Vec = possible.iter().copied().filter(|c| !eff_set.contains(c)).collect(); + if !perf.is_empty() { + return ShapingCpuDetection { + exclude_efficiency_cores: exclude, + source: ShapingCpuSource::PossibleMinusAtomSysfs, + possible, + performance: perf.clone(), + efficiency: eff, + shaping: perf, + }; + } + } + } + + // Final fallback: use all possible CPUs. + ShapingCpuDetection { + exclude_efficiency_cores: exclude, + source: possible_source, + possible: possible.clone(), + performance: possible.clone(), + efficiency: eff_opt.unwrap_or_default(), + shaping: possible, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_cpu_list_single() { + assert_eq!(parse_cpu_list("0").unwrap(), vec![0]); + } + + #[test] + fn parse_cpu_list_range() { + assert_eq!(parse_cpu_list("0-3").unwrap(), vec![0, 1, 2, 3]); + } + + #[test] + fn parse_cpu_list_mixed() { + assert_eq!(parse_cpu_list("0-3,8,10-12").unwrap(), vec![0, 1, 2, 3, 8, 10, 11, 12]); + } + + #[test] + fn parse_cpu_list_whitespace_and_dedup() { + assert_eq!(parse_cpu_list(" 0-1, 1 , 2 \n").unwrap(), vec![0, 1, 2]); + } + + #[test] + fn parse_cpu_list_invalid_range() { + assert_eq!(parse_cpu_list("3-1").unwrap_err(), CpuListParseError::InvalidRange); + } + + #[test] + fn parse_cpu_list_empty() { + assert_eq!(parse_cpu_list(" \n").unwrap_err(), CpuListParseError::Empty); + } +} + diff --git a/src/rust/lqos_config/src/etc/v15/top_config.rs b/src/rust/lqos_config/src/etc/v15/top_config.rs index 2cd112cb..955b8449 100644 --- a/src/rust/lqos_config/src/etc/v15/top_config.rs +++ b/src/rust/lqos_config/src/etc/v15/top_config.rs @@ -108,6 +108,13 @@ pub struct Config { /// Disable ICMP Ping Monitoring for Devices in the hosts view pub disable_icmp_ping: Option, + /// Exclude efficiency cores (E-cores) from CPU assignment / shaping where possible. + /// + /// On hybrid CPUs, this attempts to detect performance cores (P-cores) via sysfs and + /// restricts LibreQoS CPU binning and XDP redirection to those CPUs. + #[serde(default = "default_true")] + pub exclude_efficiency_cores: bool, + /// Enable per-circuit TemporalHeatmap collection. #[serde(default = "default_true")] pub enable_circuit_heatmaps: bool, @@ -241,6 +248,7 @@ impl Default for Config { webserver_listen: None, stormguard: None, disable_icmp_ping: Some(false), + exclude_efficiency_cores: true, enable_circuit_heatmaps: true, enable_site_heatmaps: true, enable_asn_heatmaps: true, diff --git a/src/rust/lqos_config/src/lib.rs b/src/rust/lqos_config/src/lib.rs index a8d29c72..30299326 100644 --- a/src/rust/lqos_config/src/lib.rs +++ b/src/rust/lqos_config/src/lib.rs @@ -8,6 +8,7 @@ #![deny(clippy::unwrap_used)] #![warn(missing_docs)] pub mod authentication; +mod cpu_topology; mod etc; mod network_json; mod program_control; @@ -15,6 +16,7 @@ mod qoo_profiles; mod shaped_devices; pub use authentication::{UserRole, WebUser, WebUsers}; +pub use cpu_topology::{CpuListParseError, ShapingCpuDetection, ShapingCpuSource, detect_shaping_cpus}; pub use etc::{ BridgeConfig, Config, LazyQueueMode, SingleInterfaceConfig, StormguardConfig, Tunables, disable_xdp_bridge, enable_long_term_stats, load_config, update_config, diff --git a/src/rust/lqos_python/src/lib.rs b/src/rust/lqos_python/src/lib.rs index 930630c4..1979545c 100644 --- a/src/rust/lqos_python/src/lib.rs +++ b/src/rust/lqos_python/src/lib.rs @@ -598,6 +598,7 @@ fn liblqos_python(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(enable_actual_shell_commands, m)?)?; m.add_function(wrap_pyfunction!(use_bin_packing_to_balance_cpu, m)?)?; m.add_function(wrap_pyfunction!(monitor_mode_only, m)?)?; + m.add_function(wrap_pyfunction!(shaping_cpu_count, m)?)?; m.add_function(wrap_pyfunction!(run_shell_commands_as_sudo, m)?)?; m.add_function(wrap_pyfunction!(generated_pn_download_mbps, m)?)?; m.add_function(wrap_pyfunction!(generated_pn_upload_mbps, m)?)?; @@ -1149,6 +1150,16 @@ fn monitor_mode_only() -> PyResult { Ok(config.queues.monitor_only) } +/// Returns the number of CPUs that should be used for shaping / binning. +/// +/// On hybrid CPUs, this may exclude efficiency cores (E-cores) when configured. +#[pyfunction] +fn shaping_cpu_count() -> PyResult { + let config = lqos_config::load_config().unwrap(); + let det = lqos_config::detect_shaping_cpus(config.as_ref()); + Ok(det.shaping.len() as u32) +} + #[pyfunction] fn run_shell_commands_as_sudo() -> PyResult { let config = lqos_config::load_config().unwrap(); diff --git a/src/rust/lqos_sys/src/bpf/lqos_kern.c b/src/rust/lqos_sys/src/bpf/lqos_kern.c index 5bbf4875..b1199caa 100644 --- a/src/rust/lqos_sys/src/bpf/lqos_kern.c +++ b/src/rust/lqos_sys/src/bpf/lqos_kern.c @@ -288,6 +288,12 @@ int tc_iphash_to_cpu(struct __sk_buff *skb) // We can short-circuit the redirect and bypass the second // LPM lookup! Yay! skb->priority = meta->tc_handle; + // Ensure the selected MQ queue matches the TC handle's major. + // This decouples shaping correctness from CPU selection. + __u16 major = meta->tc_handle >> 16; + if (major != 0) { + skb->queue_mapping = major; + } #ifdef TRACING { __u64 now = bpf_ktime_get_ns(); @@ -336,6 +342,11 @@ int tc_iphash_to_cpu(struct __sk_buff *skb) bpf_debug("(TC) Mapped to TC handle %x", ip_info.tc_handle); #endif skb->priority = ip_info.tc_handle; + // Ensure the selected MQ queue matches the TC handle's major. + __u16 major = ip_info.tc_handle >> 16; + if (major != 0) { + skb->queue_mapping = major; + } #ifdef TRACING { __u64 now = bpf_ktime_get_ns(); diff --git a/src/rust/lqos_sys/src/cpu_map.rs b/src/rust/lqos_sys/src/cpu_map.rs index aeb08c28..12978fe7 100644 --- a/src/rust/lqos_sys/src/cpu_map.rs +++ b/src/rust/lqos_sys/src/cpu_map.rs @@ -1,12 +1,12 @@ -use crate::{linux::map_txq_config_base_setup, num_possible_cpus}; +use crate::{linux::map_txq_config_shaping, num_possible_cpus}; use anyhow::{Error, Result}; use libbpf_sys::{bpf_map_update_elem, bpf_obj_get}; use std::{ffi::CString, os::raw::c_void}; use tracing::debug; //* Provides an interface for querying the number of CPUs eBPF can -//* see, and marking CPUs as available. Currently marks ALL eBPF -//* usable CPUs as available. +//* see, and marking CPUs as available. Callers may provide a shaping +//* CPU allowlist (e.g. exclude E-cores on hybrid CPUs). pub(crate) struct CpuMapping { fd_cpu_map: i32, @@ -33,15 +33,32 @@ impl CpuMapping { }) } - pub(crate) fn mark_cpus_available(&self) -> Result<()> { + pub(crate) fn mark_cpus_available(&self, shaping_physical_cpus: &[u32]) -> Result<()> { let cpu_count = num_possible_cpus()?; let queue_size = 2048u32; let val_ptr: *const u32 = &queue_size; - for cpu in 0..cpu_count { - debug!("Mapping core #{cpu}"); - // Insert into the cpu map - let cpu_ptr: *const u32 = &cpu; + + // Determine which physical CPUs we will redirect to. + let mut shaping: Vec = if shaping_physical_cpus.is_empty() { + (0..cpu_count).collect() + } else { + shaping_physical_cpus + .iter() + .copied() + .filter(|c| *c < cpu_count) + .collect() + }; + shaping.sort_unstable(); + shaping.dedup(); + if shaping.is_empty() { + shaping = (0..cpu_count).collect(); + } + + // Populate CPU map entries for each destination CPU (physical CPU IDs). + for cpu_dest in shaping.iter().copied() { + debug!("Mapping destination CPU #{cpu_dest} into cpumap"); + let cpu_ptr: *const u32 = &cpu_dest; let error = unsafe { bpf_map_update_elem( self.fd_cpu_map, @@ -51,27 +68,36 @@ impl CpuMapping { ) }; if error != 0 { - return Err(Error::msg("Unable to map CPU")); + return Err(Error::msg("Unable to map CPU in cpumap")); } + } - // Insert into the available list + // Populate logical->physical mapping table for ALL possible CPU indices. + // This prevents stale mappings from implicitly redirecting to CPU0 due to + // uninitialized array values. + let shaping_len = shaping.len() as u32; + for logical_cpu in 0..cpu_count { + let cpu_dest = shaping[(logical_cpu % shaping_len) as usize]; + debug!("Mapping logical CPU #{logical_cpu} -> physical CPU #{cpu_dest}"); + let key_ptr: *const u32 = &logical_cpu; + let val_ptr: *const u32 = &cpu_dest; let error = unsafe { bpf_map_update_elem( self.fd_cpu_available, - cpu_ptr as *const c_void, - cpu_ptr as *const c_void, + key_ptr as *const c_void, + val_ptr as *const c_void, 0, ) }; if error != 0 { return Err(Error::msg("Unable to add to available CPUs list")); } - } // CPU loop + } Ok(()) } - pub(crate) fn setup_base_txq_config(&self) -> Result<()> { - Ok(map_txq_config_base_setup(self.fd_txq_config)?) + pub(crate) fn setup_base_txq_config(&self, shaping_physical_cpus: &[u32]) -> Result<()> { + Ok(map_txq_config_shaping(self.fd_txq_config, shaping_physical_cpus)?) } } diff --git a/src/rust/lqos_sys/src/linux/txq_base_setup.rs b/src/rust/lqos_sys/src/linux/txq_base_setup.rs index e61afdad..4766b6c9 100644 --- a/src/rust/lqos_sys/src/linux/txq_base_setup.rs +++ b/src/rust/lqos_sys/src/linux/txq_base_setup.rs @@ -40,6 +40,65 @@ pub fn map_txq_config_base_setup(map_fd: i32) -> Result<(), MapTxqConfigError> { Ok(()) } +/// Populate `map_txq_config` for a shaping CPU set. +/// +/// The map is keyed by the **physical CPU** that will execute the TC program +/// (i.e. the destination of XDP redirection). Values set `queue_mapping` and +/// `htb_major` to match the logical queue index used by LibreQoS (major IDs +/// start at 1). +pub fn map_txq_config_shaping( + map_fd: i32, + shaping_physical_cpus: &[u32], +) -> Result<(), MapTxqConfigError> { + let possible_cpus = num_possible_cpus().map_err(|_| MapTxqConfigError::NumCpusError)?; + if map_fd < 0 { + error!("ERR: (bad map_fd:{map_fd}) cannot proceed without access to txq_config map"); + return Err(MapTxqConfigError::BadMapFd); + } + + if shaping_physical_cpus.is_empty() { + return map_txq_config_base_setup(map_fd); + } + + // Clear prior values to avoid stale queue mappings on excluded CPUs. + let mut txq_cfg = TxqConfig::default(); + for cpu in 0..possible_cpus { + let key_ptr: *const u32 = &cpu; + let val_ptr: *const TxqConfig = &txq_cfg; + let err = unsafe { + bpf_map_update_elem(map_fd, key_ptr as *const c_void, val_ptr as *mut c_void, 0) + }; + if err != 0 { + error!("Unable to clear TXQ map"); + return Err(MapTxqConfigError::BpfMapUpdateFail); + } + } + + // Program shaping CPUs: physical CPU -> queue index+1. + for (logical_idx, &physical_cpu) in shaping_physical_cpus.iter().enumerate() { + // Ignore CPUs outside the possible range. + if physical_cpu >= possible_cpus { + continue; + } + let queue_u16: u16 = (logical_idx as u16).saturating_add(1); + txq_cfg.queue_mapping = queue_u16; + txq_cfg.htb_major = queue_u16; + + let key_ptr: *const u32 = &physical_cpu; + let val_ptr: *const TxqConfig = &txq_cfg; + + let err = unsafe { + bpf_map_update_elem(map_fd, key_ptr as *const c_void, val_ptr as *mut c_void, 0) + }; + if err != 0 { + error!("Unable to update TXQ map for physical CPU {}", physical_cpu); + return Err(MapTxqConfigError::BpfMapUpdateFail); + } + } + + Ok(()) +} + #[derive(Error, Debug)] pub enum MapTxqConfigError { #[error("Unable to determine number of CPUs")] diff --git a/src/rust/lqos_sys/src/lqos_kernel.rs b/src/rust/lqos_sys/src/lqos_kernel.rs index e289f164..ef30a83d 100644 --- a/src/rust/lqos_sys/src/lqos_kernel.rs +++ b/src/rust/lqos_sys/src/lqos_kernel.rs @@ -273,11 +273,30 @@ pub fn attach_xdp_and_tc_to_interface( }; // Configure CPU Maps + let shaping_physical_cpus: Vec = match lqos_config::load_config() { + Ok(cfg) => { + let det = lqos_config::detect_shaping_cpus(cfg.as_ref()); + info!( + "CPU topology: exclude_efficiency_cores={} source={:?} possible={} performance={} efficiency={} shaping={}", + det.exclude_efficiency_cores, + det.source, + det.possible.len(), + det.performance.len(), + det.efficiency.len(), + det.shaping.len() + ); + det.shaping + } + Err(e) => { + warn!("Unable to load config for CPU topology detection: {:?}", e); + Vec::new() + } + }; { let cpu_map = CpuMapping::new()?; crate::cpu_map::xps_setup_default_disable(interface_name)?; - cpu_map.mark_cpus_available()?; - cpu_map.setup_base_txq_config()?; + cpu_map.mark_cpus_available(&shaping_physical_cpus)?; + cpu_map.setup_base_txq_config(&shaping_physical_cpus)?; } // Scope block to ensure the CPU maps are closed // Attach the TC program diff --git a/src/rust/lqosd/src/node_manager/js_build/src/config_general.js b/src/rust/lqosd/src/node_manager/js_build/src/config_general.js index dc9df39f..9a5816a3 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/config_general.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/config_general.js @@ -157,6 +157,7 @@ function updateConfig() { window.config.queue_check_period_ms = parseInt(document.getElementById("queueCheckPeriod").value); window.config.disable_webserver = document.getElementById("disableWebserver").checked; window.config.disable_icmp_ping = document.getElementById("disableIcmpPing").checked; + window.config.exclude_efficiency_cores = document.getElementById("excludeEfficiencyCores").checked; window.config.enable_circuit_heatmaps = document.getElementById("enableCircuitHeatmaps").checked; window.config.enable_site_heatmaps = document.getElementById("enableSiteHeatmaps").checked; window.config.enable_asn_heatmaps = document.getElementById("enableAsnHeatmaps").checked; @@ -193,6 +194,7 @@ loadConfig(() => { document.getElementById("disableWebserver").checked = window.config.disable_webserver ?? false; document.getElementById("webserverListen").value = window.config.webserver_listen ?? ""; document.getElementById("disableIcmpPing").checked = window.config.disable_icmp_ping ?? false; + document.getElementById("excludeEfficiencyCores").checked = window.config.exclude_efficiency_cores ?? true; document.getElementById("enableCircuitHeatmaps").checked = window.config.enable_circuit_heatmaps ?? true; document.getElementById("enableSiteHeatmaps").checked = window.config.enable_site_heatmaps ?? true; document.getElementById("enableAsnHeatmaps").checked = window.config.enable_asn_heatmaps ?? true; diff --git a/src/rust/lqosd/src/node_manager/static2/config_general.html b/src/rust/lqosd/src/node_manager/static2/config_general.html index 0cef5c76..4f7adf3b 100644 --- a/src/rust/lqosd/src/node_manager/static2/config_general.html +++ b/src/rust/lqosd/src/node_manager/static2/config_general.html @@ -89,6 +89,12 @@
Disable ICMP ping monitoring for devices in the hosts view
+
+ + +
Avoid assigning shaping work to E-cores on hybrid CPUs (recommended)
+
+ diff --git a/src/rust/lqosd/src/stick.rs b/src/rust/lqosd/src/stick.rs index a9c41d6b..6dd3d751 100644 --- a/src/rust/lqosd/src/stick.rs +++ b/src/rust/lqosd/src/stick.rs @@ -33,7 +33,9 @@ fn compute_stick_offset(config: &Config) -> Result { count_tx_queues(&interface)? }; - let cpu_count = lqos_sys::num_possible_cpus().map_err(|e| anyhow::anyhow!("{e:?}"))?; + // Keep stick_offset consistent with CPU binning / cpumap behavior. On hybrid CPUs this may + // exclude efficiency cores, reducing the number of usable shaping CPUs. + let cpu_count = lqos_config::detect_shaping_cpus(config).shaping.len() as u32; let queues_available = u32::min(queues_available, cpu_count); let queues_per_direction = queues_available / 2; @@ -83,4 +85,3 @@ fn count_tx_queues(interface: &str) -> Result { Ok(tx_queues) } } -