mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2026-10-25 14:20:05 -05:00
Feat: harden hybrid CPU detection and cache topology
This commit is contained in:
1 parent
9507be94d4
commit
2c1685ccfa
7 files changed
+690
-87
No files matched your search
Generated
+2
@@ -2021,7 +2021,9 @@ dependencies = [
|
||||
"ip_network",
|
||||
"ip_network_table",
|
||||
"lqos_utils",
|
||||
"nix",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
|
||||
@@ -19,6 +19,8 @@ toml = { workspace = true }
|
||||
lqos_utils = { path = "../lqos_utils" }
|
||||
arc-swap = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
nix = { workspace = true, features = ["sched"] }
|
||||
|
||||
# For memory debugging
|
||||
allocative.workspace = true
|
||||
|
||||
@@ -2,15 +2,22 @@
|
||||
//!
|
||||
//! 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.
|
||||
//! When enabled, detection probes several Linux and CPU-specific sources in
|
||||
//! descending order of confidence, persists the resolved split to the LibreQoS
|
||||
//! runtime directory, and reuses that cached result across processes.
|
||||
|
||||
use crate::Config;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use tracing::debug;
|
||||
|
||||
const POSSIBLE_CPUS_PATH: &str = "/sys/devices/system/cpu/possible";
|
||||
const CPU_TOPOLOGY_CACHE_FILE: &str = "cpu_topology_cache.json";
|
||||
const CPU_TOPOLOGY_CACHE_VERSION: u32 = 1;
|
||||
|
||||
const CPU_CORE_SYSFS_PATHS: [&str; 2] = [
|
||||
"/sys/bus/event_source/devices/cpu_core/cpus",
|
||||
@@ -22,16 +29,34 @@ const CPU_ATOM_SYSFS_PATHS: [&str; 2] = [
|
||||
"/sys/devices/cpu_atom/cpus",
|
||||
];
|
||||
|
||||
const CORE_TYPE_PERFORMANCE_VALUES: [u32; 2] = [1, 2];
|
||||
const CORE_TYPE_EFFICIENCY_VALUE: u32 = 3;
|
||||
const CPU_CAPACITY_EFFICIENCY_PERCENT: u32 = 80;
|
||||
const CPU_FREQ_EFFICIENCY_PERCENT: u32 = 90;
|
||||
const CPUID_CORE_TYPE_ATOM: u32 = 0x20;
|
||||
const CPUID_CORE_TYPE_CORE: u32 = 0x40;
|
||||
|
||||
static DETECTED_TOPOLOGY: Lazy<RwLock<Option<ResolvedHybridCpuTopology>>> =
|
||||
Lazy::new(|| RwLock::new(None));
|
||||
|
||||
/// Indicates which mechanism was used to choose shaping CPUs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ShapingCpuSource {
|
||||
/// Exclusion is disabled; use all possible CPUs.
|
||||
ExclusionDisabled,
|
||||
/// Used `/sys/devices/system/cpu/cpu*/topology/core_type`.
|
||||
CoreTypeSysfs,
|
||||
/// Used `/sys/devices/system/cpu/cpu*/cpu_capacity`.
|
||||
CpuCapacitySysfs,
|
||||
/// 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`.
|
||||
/// Used Intel hybrid CPUID leaf `0x1A`.
|
||||
CpuidHybrid,
|
||||
/// Used `cpuinfo_max_freq` as a last-resort heuristic.
|
||||
MaxFrequencyHeuristic,
|
||||
/// Hybrid detection failed or was not trustworthy; used `/sys/devices/system/cpu/possible`.
|
||||
FallbackAllPossible,
|
||||
/// Could not read `/sys/devices/system/cpu/possible`; used Rust's available parallelism.
|
||||
FallbackAvailableParallelism,
|
||||
@@ -44,16 +69,86 @@ pub struct ShapingCpuDetection {
|
||||
pub exclude_efficiency_cores: bool,
|
||||
/// Source used to determine the shaping CPU list.
|
||||
pub source: ShapingCpuSource,
|
||||
/// Whether the resolved topology was loaded from the persisted cache file.
|
||||
pub from_cache: bool,
|
||||
/// Whether a trustworthy hybrid split was detected.
|
||||
pub has_hybrid_split: bool,
|
||||
/// Human-readable detail used for logs and tests.
|
||||
pub detail: String,
|
||||
/// All possible CPU IDs (best effort).
|
||||
pub possible: Vec<u32>,
|
||||
/// Detected performance-core CPU IDs (may be empty if unknown).
|
||||
/// Detected performance-core CPU IDs.
|
||||
pub performance: Vec<u32>,
|
||||
/// Detected efficiency-core CPU IDs (may be empty if unknown).
|
||||
/// Detected efficiency-core CPU IDs.
|
||||
pub efficiency: Vec<u32>,
|
||||
/// Final CPU list that should be used for shaping/bins.
|
||||
pub shaping: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct ResolvedHybridCpuTopology {
|
||||
source: ShapingCpuSource,
|
||||
from_cache: bool,
|
||||
has_hybrid_split: bool,
|
||||
detail: String,
|
||||
possible: Vec<u32>,
|
||||
performance: Vec<u32>,
|
||||
efficiency: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct CpuTopologyCacheFile {
|
||||
version: u32,
|
||||
fingerprint: CpuTopologyFingerprint,
|
||||
topology: ResolvedHybridCpuTopology,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct CpuTopologyFingerprint {
|
||||
kernel_release: Option<String>,
|
||||
possible: Vec<u32>,
|
||||
vendor_id: Option<String>,
|
||||
model_name: Option<String>,
|
||||
cpu_family: Option<String>,
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
impl ResolvedHybridCpuTopology {
|
||||
fn hybrid(
|
||||
source: ShapingCpuSource,
|
||||
possible: &[u32],
|
||||
performance: Vec<u32>,
|
||||
efficiency: Vec<u32>,
|
||||
detail: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
source,
|
||||
from_cache: false,
|
||||
has_hybrid_split: true,
|
||||
detail: detail.into(),
|
||||
possible: possible.to_vec(),
|
||||
performance,
|
||||
efficiency,
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback(
|
||||
source: ShapingCpuSource,
|
||||
possible: &[u32],
|
||||
detail: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
source,
|
||||
from_cache: false,
|
||||
has_hybrid_split: false,
|
||||
detail: detail.into(),
|
||||
possible: possible.to_vec(),
|
||||
performance: possible.to_vec(),
|
||||
efficiency: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur when parsing a Linux cpulist string.
|
||||
#[derive(Error, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CpuListParseError {
|
||||
@@ -138,20 +233,415 @@ fn possible_cpu_list() -> (Vec<u32>, ShapingCpuSource) {
|
||||
(fallback, ShapingCpuSource::FallbackAvailableParallelism)
|
||||
}
|
||||
|
||||
fn try_read_u32(path: impl AsRef<Path>) -> Option<u32> {
|
||||
std::fs::read_to_string(path)
|
||||
.ok()?
|
||||
.trim()
|
||||
.parse::<u32>()
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn read_per_cpu_values(possible: &[u32], suffix: &str) -> BTreeMap<u32, u32> {
|
||||
possible
|
||||
.iter()
|
||||
.filter_map(|cpu| {
|
||||
let path = format!("/sys/devices/system/cpu/cpu{cpu}/{suffix}");
|
||||
try_read_u32(&path).map(|value| (*cpu, value))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn filter_subset(possible: &[u32], cpus: Vec<u32>) -> Vec<u32> {
|
||||
let possible_set: HashSet<u32> = possible.iter().copied().collect();
|
||||
let mut filtered: Vec<u32> = cpus
|
||||
.into_iter()
|
||||
.filter(|cpu| possible_set.contains(cpu))
|
||||
.collect();
|
||||
filtered.sort_unstable();
|
||||
filtered.dedup();
|
||||
filtered
|
||||
}
|
||||
|
||||
fn validate_split(possible: &[u32], performance: Vec<u32>, efficiency: Vec<u32>) -> Option<(Vec<u32>, Vec<u32>)> {
|
||||
let possible_set: HashSet<u32> = possible.iter().copied().collect();
|
||||
let mut perf = filter_subset(possible, performance);
|
||||
let eff = filter_subset(possible, efficiency);
|
||||
if perf.is_empty() || eff.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let eff_set: HashSet<u32> = eff.iter().copied().collect();
|
||||
perf.retain(|cpu| !eff_set.contains(cpu));
|
||||
if perf.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let union: HashSet<u32> = perf.iter().chain(eff.iter()).copied().collect();
|
||||
if union != possible_set {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((perf, eff))
|
||||
}
|
||||
|
||||
fn detect_from_core_types(possible: &[u32], values: &BTreeMap<u32, u32>) -> Option<ResolvedHybridCpuTopology> {
|
||||
if values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let performance: Vec<u32> = values
|
||||
.iter()
|
||||
.filter_map(|(cpu, value)| CORE_TYPE_PERFORMANCE_VALUES.contains(value).then_some(*cpu))
|
||||
.collect();
|
||||
let efficiency: Vec<u32> = values
|
||||
.iter()
|
||||
.filter_map(|(cpu, value)| (*value == CORE_TYPE_EFFICIENCY_VALUE).then_some(*cpu))
|
||||
.collect();
|
||||
|
||||
let (performance, efficiency) = validate_split(possible, performance, efficiency)?;
|
||||
Some(ResolvedHybridCpuTopology::hybrid(
|
||||
ShapingCpuSource::CoreTypeSysfs,
|
||||
possible,
|
||||
performance,
|
||||
efficiency,
|
||||
"Detected hybrid CPU split via topology/core_type",
|
||||
))
|
||||
}
|
||||
|
||||
fn detect_from_capacity(possible: &[u32], values: &BTreeMap<u32, u32>) -> Option<ResolvedHybridCpuTopology> {
|
||||
if values.len() != possible.len() || values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let max_capacity = values.values().copied().max()?;
|
||||
let min_capacity = values.values().copied().min()?;
|
||||
if max_capacity == 0 || max_capacity == min_capacity {
|
||||
return None;
|
||||
}
|
||||
|
||||
let threshold = max_capacity.saturating_mul(CPU_CAPACITY_EFFICIENCY_PERCENT) / 100;
|
||||
let mut performance = Vec::new();
|
||||
let mut efficiency = Vec::new();
|
||||
for (cpu, value) in values {
|
||||
if *value < threshold {
|
||||
efficiency.push(*cpu);
|
||||
} else {
|
||||
performance.push(*cpu);
|
||||
}
|
||||
}
|
||||
|
||||
let (performance, efficiency) = validate_split(possible, performance, efficiency)?;
|
||||
Some(ResolvedHybridCpuTopology::hybrid(
|
||||
ShapingCpuSource::CpuCapacitySysfs,
|
||||
possible,
|
||||
performance,
|
||||
efficiency,
|
||||
format!(
|
||||
"Detected hybrid CPU split via cpu_capacity (max={max_capacity}, min={min_capacity})"
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn detect_from_cpu_core_atom_lists(
|
||||
possible: &[u32],
|
||||
perf_opt: Option<Vec<u32>>,
|
||||
eff_opt: Option<Vec<u32>>,
|
||||
) -> Option<ResolvedHybridCpuTopology> {
|
||||
if let Some(performance) = perf_opt.clone() {
|
||||
let efficiency = eff_opt.clone().unwrap_or_default();
|
||||
if let Some((performance, efficiency)) = validate_split(possible, performance, efficiency) {
|
||||
return Some(ResolvedHybridCpuTopology::hybrid(
|
||||
ShapingCpuSource::CpuCoreSysfs,
|
||||
possible,
|
||||
performance,
|
||||
efficiency,
|
||||
"Detected hybrid CPU split via cpu_core cpulist",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(efficiency) = eff_opt {
|
||||
let possible_set: HashSet<u32> = possible.iter().copied().collect();
|
||||
let eff_set: HashSet<u32> = efficiency.iter().copied().collect();
|
||||
let performance: Vec<u32> = possible_set.difference(&eff_set).copied().collect();
|
||||
let (performance, efficiency) = validate_split(possible, performance, efficiency)?;
|
||||
return Some(ResolvedHybridCpuTopology::hybrid(
|
||||
ShapingCpuSource::PossibleMinusAtomSysfs,
|
||||
possible,
|
||||
performance,
|
||||
efficiency,
|
||||
"Detected hybrid CPU split via cpu_atom cpulist",
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn detect_from_frequency(possible: &[u32], values: &BTreeMap<u32, u32>) -> Option<ResolvedHybridCpuTopology> {
|
||||
if values.len() != possible.len() || values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let max_freq = values.values().copied().max()?;
|
||||
let min_freq = values.values().copied().min()?;
|
||||
if max_freq == 0 || max_freq == min_freq {
|
||||
return None;
|
||||
}
|
||||
|
||||
let threshold = max_freq.saturating_mul(CPU_FREQ_EFFICIENCY_PERCENT) / 100;
|
||||
let mut performance = Vec::new();
|
||||
let mut efficiency = Vec::new();
|
||||
for (cpu, value) in values {
|
||||
if *value < threshold {
|
||||
efficiency.push(*cpu);
|
||||
} else {
|
||||
performance.push(*cpu);
|
||||
}
|
||||
}
|
||||
|
||||
let (performance, efficiency) = validate_split(possible, performance, efficiency)?;
|
||||
Some(ResolvedHybridCpuTopology::hybrid(
|
||||
ShapingCpuSource::MaxFrequencyHeuristic,
|
||||
possible,
|
||||
performance,
|
||||
efficiency,
|
||||
format!(
|
||||
"Detected hybrid CPU split via cpuinfo_max_freq (max={max_freq}, min={min_freq})"
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
fn detect_from_cpuid(possible: &[u32]) -> Option<ResolvedHybridCpuTopology> {
|
||||
use nix::sched::{CpuSet, sched_getaffinity, sched_setaffinity};
|
||||
use nix::unistd::Pid;
|
||||
|
||||
#[cfg(target_arch = "x86")]
|
||||
use std::arch::x86::__cpuid_count;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use std::arch::x86_64::__cpuid_count;
|
||||
|
||||
let original_affinity = sched_getaffinity(Pid::from_raw(0)).ok()?;
|
||||
let restore_affinity = original_affinity;
|
||||
let mut performance = Vec::new();
|
||||
let mut efficiency = Vec::new();
|
||||
|
||||
for cpu in possible {
|
||||
let mut affinity = CpuSet::new();
|
||||
if affinity.set(*cpu as usize).is_err() {
|
||||
let _ = sched_setaffinity(Pid::from_raw(0), &restore_affinity);
|
||||
return None;
|
||||
}
|
||||
if sched_setaffinity(Pid::from_raw(0), &affinity).is_err() {
|
||||
let _ = sched_setaffinity(Pid::from_raw(0), &restore_affinity);
|
||||
return None;
|
||||
}
|
||||
|
||||
let leaf = unsafe { __cpuid_count(0x1a, 0) };
|
||||
let core_type = (leaf.eax >> 24) & 0xff;
|
||||
match core_type {
|
||||
CPUID_CORE_TYPE_CORE => performance.push(*cpu),
|
||||
CPUID_CORE_TYPE_ATOM => efficiency.push(*cpu),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = sched_setaffinity(Pid::from_raw(0), &restore_affinity);
|
||||
|
||||
let (performance, efficiency) = validate_split(possible, performance, efficiency)?;
|
||||
Some(ResolvedHybridCpuTopology::hybrid(
|
||||
ShapingCpuSource::CpuidHybrid,
|
||||
possible,
|
||||
performance,
|
||||
efficiency,
|
||||
"Detected hybrid CPU split via Intel hybrid CPUID leaf 0x1A",
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
|
||||
fn detect_from_cpuid(_possible: &[u32]) -> Option<ResolvedHybridCpuTopology> {
|
||||
None
|
||||
}
|
||||
|
||||
fn detect_hybrid_topology(possible: &[u32], possible_source: ShapingCpuSource) -> ResolvedHybridCpuTopology {
|
||||
let core_types = read_per_cpu_values(possible, "topology/core_type");
|
||||
if let Some(resolved) = detect_from_core_types(possible, &core_types) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
let capacities = read_per_cpu_values(possible, "cpu_capacity");
|
||||
if let Some(resolved) = detect_from_capacity(possible, &capacities) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
let perf_opt = first_cpu_list(&CPU_CORE_SYSFS_PATHS);
|
||||
let eff_opt = first_cpu_list(&CPU_ATOM_SYSFS_PATHS);
|
||||
if let Some(resolved) = detect_from_cpu_core_atom_lists(possible, perf_opt, eff_opt) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
if let Some(resolved) = detect_from_cpuid(possible) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
let frequencies = read_per_cpu_values(possible, "cpufreq/cpuinfo_max_freq");
|
||||
if let Some(resolved) = detect_from_frequency(possible, &frequencies) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
ResolvedHybridCpuTopology::fallback(
|
||||
possible_source,
|
||||
possible,
|
||||
"No trustworthy hybrid CPU split detected; using all possible CPUs",
|
||||
)
|
||||
}
|
||||
|
||||
fn cache_file_path(cfg: &Config) -> PathBuf {
|
||||
Path::new(&cfg.lqos_directory).join(CPU_TOPOLOGY_CACHE_FILE)
|
||||
}
|
||||
|
||||
fn read_cpuinfo_identity() -> (Option<String>, Option<String>, Option<String>, Option<String>) {
|
||||
let cpuinfo = match std::fs::read_to_string("/proc/cpuinfo") {
|
||||
Ok(cpuinfo) => cpuinfo,
|
||||
Err(_) => return (None, None, None, None),
|
||||
};
|
||||
|
||||
let mut vendor_id = None;
|
||||
let mut model_name = None;
|
||||
let mut cpu_family = None;
|
||||
let mut model = None;
|
||||
for line in cpuinfo.lines() {
|
||||
let Some((key, value)) = line.split_once(':') else {
|
||||
continue;
|
||||
};
|
||||
let key = key.trim();
|
||||
let value = value.trim().to_string();
|
||||
match key {
|
||||
"vendor_id" if vendor_id.is_none() => vendor_id = Some(value),
|
||||
"model name" | "Processor" | "Hardware" if model_name.is_none() => {
|
||||
model_name = Some(value);
|
||||
}
|
||||
"cpu family" if cpu_family.is_none() => cpu_family = Some(value),
|
||||
"model" if model.is_none() => model = Some(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(vendor_id, model_name, cpu_family, model)
|
||||
}
|
||||
|
||||
impl CpuTopologyFingerprint {
|
||||
fn gather(possible: &[u32]) -> Self {
|
||||
let kernel_release = std::fs::read_to_string("/proc/sys/kernel/osrelease")
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
let (vendor_id, model_name, cpu_family, model) = read_cpuinfo_identity();
|
||||
Self {
|
||||
kernel_release,
|
||||
possible: possible.to_vec(),
|
||||
vendor_id,
|
||||
model_name,
|
||||
cpu_family,
|
||||
model,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_topology_cache(
|
||||
cfg: &Config,
|
||||
fingerprint: &CpuTopologyFingerprint,
|
||||
) -> Option<ResolvedHybridCpuTopology> {
|
||||
let path = cache_file_path(cfg);
|
||||
let raw = std::fs::read_to_string(&path).ok()?;
|
||||
let cache: CpuTopologyCacheFile = serde_json::from_str(&raw).ok()?;
|
||||
if cache.version != CPU_TOPOLOGY_CACHE_VERSION || cache.fingerprint != *fingerprint {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut topology = cache.topology;
|
||||
topology.from_cache = true;
|
||||
Some(topology)
|
||||
}
|
||||
|
||||
fn store_topology_cache(
|
||||
cfg: &Config,
|
||||
fingerprint: &CpuTopologyFingerprint,
|
||||
topology: &ResolvedHybridCpuTopology,
|
||||
) {
|
||||
let path = cache_file_path(cfg);
|
||||
if let Some(parent) = path.parent()
|
||||
&& let Err(e) = std::fs::create_dir_all(parent)
|
||||
{
|
||||
debug!("Unable to create CPU topology cache directory {parent:?}: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
let temp_path = path.with_extension("json.tmp");
|
||||
let cache = CpuTopologyCacheFile {
|
||||
version: CPU_TOPOLOGY_CACHE_VERSION,
|
||||
fingerprint: fingerprint.clone(),
|
||||
topology: ResolvedHybridCpuTopology {
|
||||
from_cache: false,
|
||||
..topology.clone()
|
||||
},
|
||||
};
|
||||
|
||||
let serialized = match serde_json::to_string_pretty(&cache) {
|
||||
Ok(serialized) => serialized,
|
||||
Err(e) => {
|
||||
debug!("Unable to serialize CPU topology cache: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = std::fs::write(&temp_path, serialized.as_bytes()) {
|
||||
debug!("Unable to write temporary CPU topology cache {temp_path:?}: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = std::fs::rename(&temp_path, &path) {
|
||||
debug!("Unable to atomically replace CPU topology cache {path:?}: {e}");
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_or_detected_topology(cfg: &Config) -> ResolvedHybridCpuTopology {
|
||||
if let Some(topology) = DETECTED_TOPOLOGY.read().clone() {
|
||||
return topology;
|
||||
}
|
||||
|
||||
let (possible, possible_source) = possible_cpu_list();
|
||||
let fingerprint = CpuTopologyFingerprint::gather(&possible);
|
||||
|
||||
let resolved = load_topology_cache(cfg, &fingerprint).unwrap_or_else(|| {
|
||||
let detected = detect_hybrid_topology(&possible, possible_source);
|
||||
store_topology_cache(cfg, &fingerprint, &detected);
|
||||
detected
|
||||
});
|
||||
|
||||
*DETECTED_TOPOLOGY.write() = Some(resolved.clone());
|
||||
resolved
|
||||
}
|
||||
|
||||
/// 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".
|
||||
/// When `cfg.exclude_efficiency_cores` is enabled, this may read Linux sysfs,
|
||||
/// CPU information, and a cached topology file under the LibreQoS runtime
|
||||
/// directory. If hybrid detection fails or yields an untrustworthy split,
|
||||
/// LibreQoS falls back to all possible CPUs so non-hybrid systems are
|
||||
/// unaffected.
|
||||
pub fn detect_shaping_cpus(cfg: &Config) -> ShapingCpuDetection {
|
||||
let exclude = cfg.exclude_efficiency_cores;
|
||||
|
||||
let (possible, possible_source) = possible_cpu_list();
|
||||
|
||||
if !exclude {
|
||||
let (possible, _) = possible_cpu_list();
|
||||
return ShapingCpuDetection {
|
||||
exclude_efficiency_cores: exclude,
|
||||
exclude_efficiency_cores: false,
|
||||
source: ShapingCpuSource::ExclusionDisabled,
|
||||
from_cache: false,
|
||||
has_hybrid_split: false,
|
||||
detail: "Efficiency-core exclusion disabled".to_string(),
|
||||
possible: possible.clone(),
|
||||
performance: possible.clone(),
|
||||
efficiency: Vec::new(),
|
||||
@@ -159,75 +649,44 @@ pub fn detect_shaping_cpus(cfg: &Config) -> ShapingCpuDetection {
|
||||
};
|
||||
}
|
||||
|
||||
let perf_opt = first_cpu_list(&CPU_CORE_SYSFS_PATHS);
|
||||
let eff_opt = first_cpu_list(&CPU_ATOM_SYSFS_PATHS);
|
||||
let topology = cached_or_detected_topology(cfg);
|
||||
let performance = if topology.has_hybrid_split {
|
||||
topology.performance.clone()
|
||||
} else {
|
||||
topology.possible.clone()
|
||||
};
|
||||
let efficiency = if topology.has_hybrid_split {
|
||||
topology.efficiency.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// 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()
|
||||
&& !eff.is_empty()
|
||||
&& !perf.is_empty()
|
||||
{
|
||||
let eff_set: HashSet<u32> = 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<u32> = 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()
|
||||
&& !eff.is_empty()
|
||||
&& !possible.is_empty()
|
||||
{
|
||||
let eff_set: HashSet<u32> = eff.iter().copied().collect();
|
||||
let perf: Vec<u32> = 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,
|
||||
exclude_efficiency_cores: true,
|
||||
source: topology.source,
|
||||
from_cache: topology.from_cache,
|
||||
has_hybrid_split: topology.has_hybrid_split,
|
||||
detail: topology.detail,
|
||||
possible: topology.possible.clone(),
|
||||
performance: performance.clone(),
|
||||
efficiency,
|
||||
shaping: performance,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn temp_dir(name: &str) -> PathBuf {
|
||||
let unique = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!("libreqos-{name}-{unique}"));
|
||||
std::fs::create_dir_all(&dir).expect("test temp dir should be creatable");
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_cpu_list_single() {
|
||||
@@ -276,4 +735,140 @@ mod tests {
|
||||
CpuListParseError::Empty
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_core_type_split() {
|
||||
let possible = vec![0, 1, 2, 3];
|
||||
let values = BTreeMap::from([(0, 1), (1, 2), (2, 3), (3, 3)]);
|
||||
let detection =
|
||||
detect_from_core_types(&possible, &values).expect("core_type should detect hybrid");
|
||||
assert_eq!(detection.source, ShapingCpuSource::CoreTypeSysfs);
|
||||
assert_eq!(detection.performance, vec![0, 1]);
|
||||
assert_eq!(detection.efficiency, vec![2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_cpu_capacity_split() {
|
||||
let possible = vec![0, 1, 2, 3];
|
||||
let values = BTreeMap::from([(0, 1024), (1, 1024), (2, 512), (3, 512)]);
|
||||
let detection =
|
||||
detect_from_capacity(&possible, &values).expect("cpu_capacity should detect hybrid");
|
||||
assert_eq!(detection.source, ShapingCpuSource::CpuCapacitySysfs);
|
||||
assert_eq!(detection.performance, vec![0, 1]);
|
||||
assert_eq!(detection.efficiency, vec![2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capacity_rejects_non_partitioned_split() {
|
||||
let possible = vec![0, 1, 2, 3];
|
||||
let values = BTreeMap::from([(0, 1024), (1, 1024), (2, 512)]);
|
||||
assert!(
|
||||
detect_from_capacity(&possible, &values).is_none(),
|
||||
"partial capacity data should not be trusted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_cpu_core_atom_lists() {
|
||||
let possible = vec![0, 1, 2, 3];
|
||||
let detection = detect_from_cpu_core_atom_lists(
|
||||
&possible,
|
||||
Some(vec![0, 1]),
|
||||
Some(vec![2, 3]),
|
||||
)
|
||||
.expect("cpu_core/cpu_atom should detect hybrid");
|
||||
assert_eq!(detection.source, ShapingCpuSource::CpuCoreSysfs);
|
||||
assert_eq!(detection.performance, vec![0, 1]);
|
||||
assert_eq!(detection.efficiency, vec![2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_frequency_split() {
|
||||
let possible = vec![0, 1, 2, 3];
|
||||
let values = BTreeMap::from([(0, 5200000), (1, 5200000), (2, 3900000), (3, 3900000)]);
|
||||
let detection =
|
||||
detect_from_frequency(&possible, &values).expect("frequency split should detect hybrid");
|
||||
assert_eq!(detection.source, ShapingCpuSource::MaxFrequencyHeuristic);
|
||||
assert_eq!(detection.performance, vec![0, 1]);
|
||||
assert_eq!(detection.efficiency, vec![2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_uses_all_possible_cpus() {
|
||||
let possible = vec![0, 1, 2, 3];
|
||||
let detection = detect_hybrid_topology(&possible, ShapingCpuSource::FallbackAllPossible);
|
||||
assert_eq!(detection.source, ShapingCpuSource::FallbackAllPossible);
|
||||
assert!(!detection.has_hybrid_split);
|
||||
assert_eq!(detection.performance, possible);
|
||||
assert!(detection.efficiency.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn topology_cache_round_trip() {
|
||||
let cfg = Config {
|
||||
lqos_directory: temp_dir("cpu-topology-cache").display().to_string(),
|
||||
..Config::default()
|
||||
};
|
||||
let path = cache_file_path(&cfg);
|
||||
let fingerprint = CpuTopologyFingerprint {
|
||||
kernel_release: Some("6.9.0".to_string()),
|
||||
possible: vec![0, 1, 2, 3],
|
||||
vendor_id: Some("GenuineIntel".to_string()),
|
||||
model_name: Some("Test CPU".to_string()),
|
||||
cpu_family: Some("6".to_string()),
|
||||
model: Some("151".to_string()),
|
||||
};
|
||||
let topology = ResolvedHybridCpuTopology::hybrid(
|
||||
ShapingCpuSource::CoreTypeSysfs,
|
||||
&[0, 1, 2, 3],
|
||||
vec![0, 1],
|
||||
vec![2, 3],
|
||||
"test topology",
|
||||
);
|
||||
|
||||
store_topology_cache(&cfg, &fingerprint, &topology);
|
||||
let loaded = load_topology_cache(&cfg, &fingerprint).expect("cache should round-trip");
|
||||
assert!(loaded.from_cache);
|
||||
assert_eq!(loaded.source, topology.source);
|
||||
assert_eq!(loaded.performance, topology.performance);
|
||||
assert_eq!(loaded.efficiency, topology.efficiency);
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn topology_cache_invalidates_on_fingerprint_change() {
|
||||
let cfg = Config {
|
||||
lqos_directory: temp_dir("cpu-topology-cache-mismatch")
|
||||
.display()
|
||||
.to_string(),
|
||||
..Config::default()
|
||||
};
|
||||
let path = cache_file_path(&cfg);
|
||||
let fingerprint = CpuTopologyFingerprint {
|
||||
kernel_release: Some("6.9.0".to_string()),
|
||||
possible: vec![0, 1, 2, 3],
|
||||
vendor_id: Some("GenuineIntel".to_string()),
|
||||
model_name: Some("Test CPU".to_string()),
|
||||
cpu_family: Some("6".to_string()),
|
||||
model: Some("151".to_string()),
|
||||
};
|
||||
let topology = ResolvedHybridCpuTopology::hybrid(
|
||||
ShapingCpuSource::CoreTypeSysfs,
|
||||
&[0, 1, 2, 3],
|
||||
vec![0, 1],
|
||||
vec![2, 3],
|
||||
"test topology",
|
||||
);
|
||||
store_topology_cache(&cfg, &fingerprint, &topology);
|
||||
|
||||
let mut mismatched = fingerprint.clone();
|
||||
mismatched.kernel_release = Some("6.10.0".to_string());
|
||||
assert!(
|
||||
load_topology_cache(&cfg, &mismatched).is_none(),
|
||||
"fingerprint mismatch should invalidate the cache"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
@@ -163,8 +163,9 @@ pub struct Config {
|
||||
|
||||
/// 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.
|
||||
/// On hybrid CPUs, this tries several detection methods, caches the resolved
|
||||
/// P-core/E-core split under the LibreQoS runtime directory, and restricts
|
||||
/// shaping/XDP CPU assignment to performance cores when the split is trustworthy.
|
||||
#[serde(default = "default_true")]
|
||||
pub exclude_efficiency_cores: bool,
|
||||
|
||||
|
||||
@@ -1427,7 +1427,8 @@ fn monitor_mode_only() -> PyResult<bool> {
|
||||
|
||||
/// Returns the number of CPUs that should be used for shaping / binning.
|
||||
///
|
||||
/// On hybrid CPUs, this may exclude efficiency cores (E-cores) when configured.
|
||||
/// On hybrid CPUs, this may exclude efficiency cores (E-cores) when configured
|
||||
/// using the cached multi-method topology detector.
|
||||
#[pyfunction]
|
||||
fn shaping_cpu_count() -> PyResult<u32> {
|
||||
let config = lqos_config::load_config().unwrap();
|
||||
@@ -1440,12 +1441,11 @@ fn shaping_cpu_count() -> PyResult<u32> {
|
||||
fn efficiency_core_ids() -> PyResult<Vec<u32>> {
|
||||
let config = lqos_config::load_config().unwrap();
|
||||
let det = lqos_config::detect_shaping_cpus(config.as_ref());
|
||||
let efficiency = match det.source {
|
||||
lqos_config::ShapingCpuSource::CpuCoreSysfs
|
||||
| lqos_config::ShapingCpuSource::PossibleMinusAtomSysfs => det.efficiency,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
Ok(efficiency)
|
||||
Ok(if det.has_hybrid_split {
|
||||
det.efficiency
|
||||
} else {
|
||||
Vec::new()
|
||||
})
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
|
||||
@@ -278,13 +278,16 @@ pub fn attach_xdp_and_tc_to_interface(
|
||||
Ok(cfg) => {
|
||||
let det = lqos_config::detect_shaping_cpus(cfg.as_ref());
|
||||
info!(
|
||||
"CPU topology: exclude_efficiency_cores={} source={:?} possible={} performance={} efficiency={} shaping={}",
|
||||
"CPU topology: exclude_efficiency_cores={} source={:?} from_cache={} hybrid_split={} possible={} performance={} efficiency={} shaping={} detail={}",
|
||||
det.exclude_efficiency_cores,
|
||||
det.source,
|
||||
det.from_cache,
|
||||
det.has_hybrid_split,
|
||||
det.possible.len(),
|
||||
det.performance.len(),
|
||||
det.efficiency.len(),
|
||||
det.shaping.len()
|
||||
det.shaping.len(),
|
||||
det.detail
|
||||
);
|
||||
det.shaping
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="excludeEfficiencyCores">
|
||||
<label class="form-check-label" for="excludeEfficiencyCores">Exclude Efficiency Cores</label>
|
||||
<div class="form-text">Avoid assigning shaping work to E-cores on hybrid CPUs (recommended)</div>
|
||||
<div class="form-text">Use cached hybrid-core detection to keep shaping on P-cores and leave E-cores for background work when available</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
|
||||
|
||||
Reference in new issue
Block a user