use std::time::Duration;
use serde::{Deserialize, Serialize};
use super::dump::FailureDumpReport;
use crate::workload::humantime_serde_helper;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[allow(dead_code)] pub struct DebugCapture {
pub schema: String,
pub started_ns: u64,
pub ended_ns: u64,
pub kernel_release: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ctprof_samples: Vec<CtprofSampleRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failure_dump: Option<FailureDumpReport>,
pub fingerprint: WorkloadFingerprint,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CtprofSampleRef {
pub captured_ns: u64,
pub path: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub sha256: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct WorkloadFingerprint {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workload_groups: Vec<WorkloadGroupHint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub affinity_hints: Vec<AffinityHint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub work_type_hints: Vec<WorkTypeHint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cgroup_hints: Vec<CgroupHint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sched_policy_hints: Vec<SchedPolicyHint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub gaps: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct WorkloadGroupHint {
pub cgroup_path: String,
pub thread_count: u32,
pub cpu_time_fraction: f64,
pub wakeups_per_sec: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(tag = "kind")]
pub enum AffinityHint {
#[default]
Inherit,
SingleCpu {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
cpus: Vec<u32>,
},
LlcAligned {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
cpus: Vec<u32>,
},
CrossCgroup {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
cpus: Vec<u32>,
},
SmtSiblingPair {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
cpus: Vec<u32>,
},
Exact { cpus: Vec<u32> },
RandomSubset {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
from: Vec<u32>,
#[serde(default, skip_serializing_if = "is_zero_u32")]
count: u32,
},
}
impl AffinityHint {
pub fn single_cpu(cpus: impl IntoIterator<Item = u32>) -> Self {
Self::SingleCpu {
cpus: cpus.into_iter().collect(),
}
}
pub fn llc_aligned(cpus: impl IntoIterator<Item = u32>) -> Self {
Self::LlcAligned {
cpus: cpus.into_iter().collect(),
}
}
pub fn cross_cgroup(cpus: impl IntoIterator<Item = u32>) -> Self {
Self::CrossCgroup {
cpus: cpus.into_iter().collect(),
}
}
pub fn smt_sibling_pair(cpus: impl IntoIterator<Item = u32>) -> Self {
Self::SmtSiblingPair {
cpus: cpus.into_iter().collect(),
}
}
pub fn exact(cpus: impl IntoIterator<Item = u32>) -> Self {
Self::Exact {
cpus: cpus.into_iter().collect(),
}
}
pub fn random_subset(from: impl IntoIterator<Item = u32>, count: u32) -> Self {
Self::RandomSubset {
from: from.into_iter().collect(),
count,
}
}
pub fn random_subset_unresolved() -> Self {
Self::RandomSubset {
from: Vec::new(),
count: 0,
}
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_zero_u32(v: &u32) -> bool {
*v == 0
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(tag = "kind")]
pub enum WorkTypeHint {
SpinWait,
YieldHeavy,
Mixed,
Bursty {
#[serde(with = "humantime_serde_helper")]
burst_duration: Duration,
#[serde(with = "humantime_serde_helper")]
sleep_duration: Duration,
},
PipeIo,
FutexPingPong,
CachePressure { size_kb: u32, stride: u32 },
IoSyncWrite,
IoRandRead,
IoConvoy,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CgroupHint {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_weight: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory_max_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cpuset_cpus: Vec<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_max_quota_us: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(tag = "policy")]
pub enum SchedPolicyHint {
Other { nice: i32 },
Fifo { priority: u32 },
RoundRobin { priority: u32 },
Deadline {
runtime_ns: u64,
deadline_ns: u64,
period_ns: u64,
},
Batch,
Idle,
Ext,
}
#[allow(dead_code)] pub const DEBUG_CAPTURE_SCHEMA: &str = "ktstr.debug_capture/v1";
#[allow(dead_code)]
pub fn project_fingerprint(
samples: &[crate::ctprof::CtprofSnapshot],
dump: Option<&FailureDumpReport>,
) -> WorkloadFingerprint {
let mut fp = WorkloadFingerprint::default();
if samples.is_empty() && dump.is_none() {
fp.gaps.push("no inputs to project from".to_string());
return fp;
}
if let Some(latest) = samples.last() {
for (cgroup_path, group_hint) in cgroup_thread_groups(latest) {
fp.workload_groups.push(WorkloadGroupHint {
cgroup_path,
thread_count: group_hint.thread_count,
cpu_time_fraction: group_hint.cpu_time_fraction,
wakeups_per_sec: group_hint.wakeups_per_sec,
});
}
}
if let Some(d) = dump {
fp.affinity_hints = project_affinity_hints(d);
fp.sched_policy_hints = project_sched_policy_hints(d);
} else {
fp.gaps
.push("affinity + sched_policy hints unavailable (no failure dump)".to_string());
}
fp.work_type_hints = project_work_type_hints(samples);
fp.cgroup_hints = project_cgroup_hints(samples);
fp
}
#[derive(Debug, Default)]
struct CgroupGroupAcc {
thread_count: u32,
cpu_time_fraction: f64,
wakeups_per_sec: f64,
}
fn cgroup_thread_groups(
_snapshot: &crate::ctprof::CtprofSnapshot,
) -> Vec<(String, CgroupGroupAcc)> {
Vec::new()
}
fn project_affinity_hints(_dump: &FailureDumpReport) -> Vec<AffinityHint> {
Vec::new()
}
fn project_sched_policy_hints(dump: &FailureDumpReport) -> Vec<SchedPolicyHint> {
let mut hints = Vec::new();
for task in &dump.task_enrichments {
let class = task.sched_class.as_deref().unwrap_or("");
let policy = match class {
"rt" if task.rt_priority > 0 => {
SchedPolicyHint::Fifo {
priority: task.rt_priority,
}
}
"dl" => SchedPolicyHint::Deadline {
runtime_ns: 0,
deadline_ns: 0,
period_ns: 0,
},
"ext" => SchedPolicyHint::Ext,
"idle" => SchedPolicyHint::Idle,
_ => {
let raw = task.static_prio - 120;
let nice = raw.clamp(-20, 19);
SchedPolicyHint::Other { nice }
}
};
hints.push(policy);
}
hints
}
fn project_work_type_hints(_samples: &[crate::ctprof::CtprofSnapshot]) -> Vec<WorkTypeHint> {
Vec::new()
}
fn project_cgroup_hints(_samples: &[crate::ctprof::CtprofSnapshot]) -> Vec<CgroupHint> {
Vec::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn project_fingerprint_no_inputs() {
let fp = project_fingerprint(&[], None);
assert!(fp.workload_groups.is_empty());
assert!(fp.affinity_hints.is_empty());
assert!(fp.work_type_hints.is_empty());
assert!(fp.cgroup_hints.is_empty());
assert!(fp.sched_policy_hints.is_empty());
assert_eq!(fp.gaps, vec!["no inputs to project from".to_string()]);
}
#[test]
fn project_sched_policy_hints_from_dump() {
use crate::monitor::task_enrichment::TaskEnrichment;
fn make_task(
pid: i32,
sched_class: &str,
rt_priority: u32,
static_prio: i32,
) -> TaskEnrichment {
TaskEnrichment {
pid,
tgid: 0,
comm: String::new(),
group_leader_pid: None,
real_parent_pid: None,
real_parent_comm: None,
pgid: None,
sid: None,
nr_threads: None,
weight: 0,
prio: 0,
static_prio,
normal_prio: 0,
rt_priority,
sched_class: Some(sched_class.to_string()),
core_cookie: None,
pi_boosted_out_of_scx: false,
nvcsw: 0,
nivcsw: 0,
signal_nvcsw: None,
signal_nivcsw: None,
lock_slowpath_match: None,
}
}
let dump = FailureDumpReport {
task_enrichments: vec![
make_task(100, "rt", 50, 0),
make_task(101, "ext", 0, 0),
make_task(102, "fair", 0, 120),
make_task(103, "fair", 0, 0),
],
..FailureDumpReport::default()
};
let fp = project_fingerprint(&[], Some(&dump));
assert_eq!(fp.sched_policy_hints.len(), 4);
match &fp.sched_policy_hints[0] {
SchedPolicyHint::Fifo { priority } => assert_eq!(*priority, 50),
other => panic!("expected Fifo, got {other:?}"),
}
match &fp.sched_policy_hints[1] {
SchedPolicyHint::Ext => {}
other => panic!("expected Ext, got {other:?}"),
}
match &fp.sched_policy_hints[2] {
SchedPolicyHint::Other { nice } => assert_eq!(*nice, 0),
other => panic!("expected Other, got {other:?}"),
}
match &fp.sched_policy_hints[3] {
SchedPolicyHint::Other { nice } => assert_eq!(
*nice, -20,
"static_prio=0 (zero-init) must clamp to MIN_NICE=-20, got nice={nice}",
),
other => panic!("expected Other, got {other:?}"),
}
}
#[test]
fn project_sched_policy_short_names_match_long_names_fall_through() {
use crate::monitor::task_enrichment::TaskEnrichment;
fn make_task(class: &str, rt_priority: u32, static_prio: i32) -> TaskEnrichment {
TaskEnrichment {
pid: 0,
tgid: 0,
comm: String::new(),
group_leader_pid: None,
real_parent_pid: None,
real_parent_comm: None,
pgid: None,
sid: None,
nr_threads: None,
weight: 0,
prio: 0,
static_prio,
normal_prio: 0,
rt_priority,
sched_class: Some(class.to_string()),
core_cookie: None,
pi_boosted_out_of_scx: false,
nvcsw: 0,
nivcsw: 0,
signal_nvcsw: None,
signal_nivcsw: None,
lock_slowpath_match: None,
}
}
let dump = FailureDumpReport {
task_enrichments: vec![
make_task("rt", 75, 0),
make_task("dl", 0, 0),
make_task("ext", 0, 0),
make_task("idle", 0, 0),
],
..FailureDumpReport::default()
};
let fp = project_fingerprint(&[], Some(&dump));
assert_eq!(fp.sched_policy_hints.len(), 4);
assert!(
matches!(
fp.sched_policy_hints[0],
SchedPolicyHint::Fifo { priority: 75 }
),
"short name 'rt' must project to Fifo, got {:?}",
fp.sched_policy_hints[0],
);
assert!(
matches!(fp.sched_policy_hints[1], SchedPolicyHint::Deadline { .. }),
"short name 'dl' must project to Deadline, got {:?}",
fp.sched_policy_hints[1],
);
assert!(
matches!(fp.sched_policy_hints[2], SchedPolicyHint::Ext),
"short name 'ext' must project to Ext, got {:?}",
fp.sched_policy_hints[2],
);
assert!(
matches!(fp.sched_policy_hints[3], SchedPolicyHint::Idle),
"short name 'idle' must project to Idle, got {:?}",
fp.sched_policy_hints[3],
);
let long_names_dump = FailureDumpReport {
task_enrichments: vec![
make_task("rt_sched_class", 75, 120),
make_task("dl_sched_class", 0, 120),
make_task("ext_sched_class", 0, 120),
make_task("idle_sched_class", 0, 120),
],
..FailureDumpReport::default()
};
let long_fp = project_fingerprint(&[], Some(&long_names_dump));
assert_eq!(long_fp.sched_policy_hints.len(), 4);
for (i, hint) in long_fp.sched_policy_hints.iter().enumerate() {
assert!(
matches!(hint, SchedPolicyHint::Other { .. }),
"long kernel symbol name at index {i} must NOT match \
any specialised arm (regression guard); got {hint:?}",
);
}
}
#[test]
fn schema_constant_pinned() {
assert_eq!(DEBUG_CAPTURE_SCHEMA, "ktstr.debug_capture/v1");
}
#[test]
fn debug_capture_serde_minimal_skips_defaults() {
let cap = DebugCapture {
schema: DEBUG_CAPTURE_SCHEMA.to_string(),
started_ns: 0,
ended_ns: 0,
kernel_release: "test-6.16".to_string(),
ctprof_samples: Vec::new(),
failure_dump: None,
fingerprint: WorkloadFingerprint::default(),
};
let json = serde_json::to_string(&cap).unwrap();
assert!(!json.contains("ctprof_samples"));
assert!(!json.contains("failure_dump"));
assert!(json.contains("schema"));
assert!(json.contains("kernel_release"));
assert!(json.contains("test-6.16"));
}
#[test]
fn fingerprint_all_hints_roundtrip() {
let fp = WorkloadFingerprint {
workload_groups: vec![WorkloadGroupHint {
cgroup_path: "/system.slice/foo.service".into(),
thread_count: 8,
cpu_time_fraction: 0.75,
wakeups_per_sec: 1200.0,
}],
affinity_hints: vec![
AffinityHint::SingleCpu { cpus: Vec::new() },
AffinityHint::Exact {
cpus: vec![0, 1, 2, 3],
},
],
work_type_hints: vec![
WorkTypeHint::SpinWait,
WorkTypeHint::Bursty {
burst_duration: Duration::from_millis(10),
sleep_duration: Duration::from_millis(90),
},
],
cgroup_hints: vec![CgroupHint {
path: "/system.slice/foo.service".into(),
cpu_weight: Some(200),
memory_max_bytes: Some(8 * 1024 * 1024 * 1024),
cpuset_cpus: vec![0, 1, 2, 3],
cpu_max_quota_us: None,
}],
sched_policy_hints: vec![
SchedPolicyHint::Fifo { priority: 50 },
SchedPolicyHint::Other { nice: 5 },
],
gaps: vec!["affinity hint backed by 1 sample".into()],
};
let json = serde_json::to_string(&fp).unwrap();
let parsed: WorkloadFingerprint = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.workload_groups.len(), 1);
assert_eq!(parsed.affinity_hints.len(), 2);
assert_eq!(parsed.work_type_hints.len(), 2);
assert_eq!(parsed.cgroup_hints.len(), 1);
assert_eq!(parsed.sched_policy_hints.len(), 2);
assert_eq!(parsed.gaps.len(), 1);
}
#[test]
fn affinity_hint_resolved_payload_roundtrips() {
let hints = [
AffinityHint::Inherit,
AffinityHint::SingleCpu { cpus: Vec::new() },
AffinityHint::SingleCpu { cpus: vec![3] },
AffinityHint::LlcAligned { cpus: Vec::new() },
AffinityHint::LlcAligned {
cpus: vec![0, 1, 2, 3],
},
AffinityHint::CrossCgroup { cpus: Vec::new() },
AffinityHint::CrossCgroup {
cpus: vec![4, 5, 6, 7],
},
AffinityHint::SmtSiblingPair { cpus: Vec::new() },
AffinityHint::SmtSiblingPair { cpus: vec![2, 3] },
AffinityHint::Exact {
cpus: vec![0, 1, 2, 3],
},
AffinityHint::RandomSubset {
from: Vec::new(),
count: 0,
},
AffinityHint::RandomSubset {
from: vec![0, 1, 2, 3, 4, 5],
count: 3,
},
];
for hint in &hints {
let json = serde_json::to_string(hint).expect("AffinityHint must serialize");
let back: AffinityHint =
serde_json::from_str(&json).expect("AffinityHint must deserialize");
match (hint, &back) {
(AffinityHint::Inherit, AffinityHint::Inherit) => {}
(AffinityHint::SingleCpu { cpus: a }, AffinityHint::SingleCpu { cpus: b }) => {
assert_eq!(a, b, "SingleCpu cpus must round-trip")
}
(AffinityHint::LlcAligned { cpus: a }, AffinityHint::LlcAligned { cpus: b }) => {
assert_eq!(a, b, "LlcAligned cpus must round-trip")
}
(AffinityHint::CrossCgroup { cpus: a }, AffinityHint::CrossCgroup { cpus: b }) => {
assert_eq!(a, b, "CrossCgroup cpus must round-trip")
}
(
AffinityHint::SmtSiblingPair { cpus: a },
AffinityHint::SmtSiblingPair { cpus: b },
) => assert_eq!(a, b, "SmtSiblingPair cpus must round-trip"),
(AffinityHint::Exact { cpus: a }, AffinityHint::Exact { cpus: b }) => {
assert_eq!(a, b, "Exact cpus must round-trip")
}
(
AffinityHint::RandomSubset {
from: pa,
count: ca,
},
AffinityHint::RandomSubset {
from: pb,
count: cb,
},
) => {
assert_eq!(pa, pb, "RandomSubset from must round-trip");
assert_eq!(ca, cb, "RandomSubset count must round-trip");
}
_ => panic!("AffinityHint round-trip mismatch: sent {hint:?}, got {back:?}",),
}
}
}
#[test]
fn work_type_hint_io_variants_roundtrip() {
for hint in [
WorkTypeHint::IoSyncWrite,
WorkTypeHint::IoRandRead,
WorkTypeHint::IoConvoy,
] {
let json = serde_json::to_string(&hint).expect("WorkTypeHint must serialize");
let back: WorkTypeHint =
serde_json::from_str(&json).expect("WorkTypeHint must deserialize");
match (&hint, &back) {
(WorkTypeHint::IoSyncWrite, WorkTypeHint::IoSyncWrite) => {}
(WorkTypeHint::IoRandRead, WorkTypeHint::IoRandRead) => {}
(WorkTypeHint::IoConvoy, WorkTypeHint::IoConvoy) => {}
_ => panic!("IO hint roundtrip mismatch: sent {hint:?}, got {back:?}",),
}
}
}
#[test]
fn affinity_hint_constructors_match_struct_literal() {
assert!(matches!(
AffinityHint::single_cpu([3u32]),
AffinityHint::SingleCpu { cpus } if cpus == vec![3]
));
assert!(matches!(
AffinityHint::single_cpu([] as [u32; 0]),
AffinityHint::SingleCpu { cpus } if cpus.is_empty()
));
assert!(matches!(
AffinityHint::llc_aligned(0u32..4),
AffinityHint::LlcAligned { cpus } if cpus == vec![0, 1, 2, 3]
));
assert!(matches!(
AffinityHint::llc_aligned(Vec::<u32>::new()),
AffinityHint::LlcAligned { cpus } if cpus.is_empty()
));
assert!(matches!(
AffinityHint::cross_cgroup(vec![4u32, 5, 6, 7]),
AffinityHint::CrossCgroup { cpus } if cpus == vec![4, 5, 6, 7]
));
assert!(matches!(
AffinityHint::cross_cgroup(Vec::<u32>::new()),
AffinityHint::CrossCgroup { cpus } if cpus.is_empty()
));
assert!(matches!(
AffinityHint::smt_sibling_pair([2u32, 3]),
AffinityHint::SmtSiblingPair { cpus } if cpus == vec![2, 3]
));
assert!(matches!(
AffinityHint::smt_sibling_pair(Vec::<u32>::new()),
AffinityHint::SmtSiblingPair { cpus } if cpus.is_empty()
));
assert!(matches!(
AffinityHint::exact([0u32, 1, 2]),
AffinityHint::Exact { cpus } if cpus == vec![0, 1, 2]
));
assert!(matches!(
AffinityHint::random_subset([0u32, 1, 2, 3, 4, 5], 3),
AffinityHint::RandomSubset { from, count }
if from == vec![0, 1, 2, 3, 4, 5] && count == 3
));
assert!(matches!(
AffinityHint::random_subset_unresolved(),
AffinityHint::RandomSubset { from, count }
if from.is_empty() && count == 0
));
}
}