use std::collections::BTreeSet;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use super::debug_capture::{
AffinityHint, CgroupHint, DebugCapture, SchedPolicyHint, WorkTypeHint, WorkloadFingerprint,
WorkloadGroupHint,
};
use crate::workload::{AffinityIntent, SchedPolicy, WorkType, WorkloadConfig};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[allow(dead_code)] pub struct ReproducerSpec {
#[serde(skip)]
pub config: WorkloadConfig,
pub cgroup_hints: Vec<CgroupHint>,
pub notes: Vec<ReproducerNote>,
pub scheduler_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "message", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ReproducerNote {
Informational(String),
Resolved(String),
UnresolvedAffinity(String),
UnmappedWorkType(String),
}
impl ReproducerNote {
#[allow(dead_code)]
pub fn message(&self) -> &str {
match self {
ReproducerNote::Informational(s)
| ReproducerNote::Resolved(s)
| ReproducerNote::UnresolvedAffinity(s)
| ReproducerNote::UnmappedWorkType(s) => s,
}
}
fn is_unresolved(&self) -> bool {
matches!(
self,
ReproducerNote::UnresolvedAffinity(_) | ReproducerNote::UnmappedWorkType(_)
)
}
}
impl std::fmt::Display for ReproducerNote {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.message())
}
}
impl ReproducerSpec {
#[allow(dead_code)]
pub fn is_runnable(&self) -> bool {
self.unresolved_count() == 0 && !is_unmapped_work_type(&self.config.work_type)
}
#[allow(dead_code)]
pub fn unresolved_count(&self) -> usize {
self.notes.iter().filter(|n| n.is_unresolved()).count()
}
}
#[allow(dead_code)]
pub fn generate_spec(capture: &DebugCapture) -> ReproducerSpec {
let mut spec = ReproducerSpec {
scheduler_name: failure_scheduler_name(capture),
..Default::default()
};
map_workload_groups(&capture.fingerprint, &mut spec);
map_affinity(&capture.fingerprint, &mut spec);
map_work_type(&capture.fingerprint, &mut spec);
map_sched_policy(&capture.fingerprint, &mut spec);
spec.cgroup_hints = capture.fingerprint.cgroup_hints.clone();
for gap in &capture.fingerprint.gaps {
spec.notes.push(ReproducerNote::Informational(format!(
"fingerprint gap: {gap}"
)));
}
spec
}
fn failure_scheduler_name(capture: &DebugCapture) -> String {
let _ = capture;
String::new()
}
fn map_workload_groups(fp: &WorkloadFingerprint, spec: &mut ReproducerSpec) {
let Some(primary) = fp.workload_groups.first() else {
spec.notes.push(ReproducerNote::Informational(
"no workload groups in fingerprint — defaulting num_workers=1".into(),
));
return;
};
spec.config.num_workers = primary.thread_count.max(1) as usize;
push_extras_note(
&mut spec.notes,
"additional workload groups not modeled in primary spec",
fp.workload_groups
.iter()
.skip(1)
.map(|g: &WorkloadGroupHint| format!("{} ({} threads)", g.cgroup_path, g.thread_count)),
);
}
fn push_extras_note(
notes: &mut Vec<ReproducerNote>,
header: &str,
entries: impl Iterator<Item = String>,
) {
let alts: Vec<String> = entries.collect();
if !alts.is_empty() {
notes.push(ReproducerNote::Informational(format!(
"{header}: {}",
alts.join(", ")
)));
}
}
fn topology_aware_note(variant: &str, engine_action: &str, hand_edit_target: &str) -> String {
format!(
"AffinityHint::{variant} observed without resolved CPUs; \
emitting AffinityIntent::{variant} — the scenario engine \
{engine_action} at apply time. The spawn-time affinity gate \
rejects this variant (no topology context); use the \
scenario engine or hand-edit to {hand_edit_target}"
)
}
fn topology_resolved_note(variant: &str, cpus: &[u32]) -> String {
format!(
"AffinityHint::{variant} observed with resolved CPUs {cpus:?}; \
emitting AffinityIntent::Exact directly so the spec runs \
without scenario-engine resolution",
)
}
fn cpus_to_set(cpus: &[u32]) -> BTreeSet<usize> {
cpus.iter().map(|&c| c as usize).collect()
}
fn map_topology_aware_affinity(
cpus: &[u32],
variant: &str,
topology_intent: AffinityIntent,
engine_action: &str,
hand_edit_target: &str,
spec: &mut ReproducerSpec,
) -> AffinityIntent {
if cpus.is_empty() {
spec.notes
.push(ReproducerNote::UnresolvedAffinity(topology_aware_note(
variant,
engine_action,
hand_edit_target,
)));
topology_intent
} else {
spec.notes
.push(ReproducerNote::Resolved(topology_resolved_note(
variant, cpus,
)));
AffinityIntent::Exact(cpus_to_set(cpus))
}
}
fn map_affinity(fp: &WorkloadFingerprint, spec: &mut ReproducerSpec) {
let Some(primary) = fp.affinity_hints.first() else {
return;
};
spec.config.affinity = match primary {
AffinityHint::Inherit => AffinityIntent::Inherit,
AffinityHint::SingleCpu { cpus } => map_topology_aware_affinity(
cpus,
"SingleCpu",
AffinityIntent::SingleCpu,
"picks the concrete CPU from the cgroup's cpuset",
"AffinityIntent::exact([<cpu>])",
spec,
),
AffinityHint::LlcAligned { cpus } => map_topology_aware_affinity(
cpus,
"LlcAligned",
AffinityIntent::LlcAligned,
"resolves the LLC mask from the cgroup's cpuset",
"AffinityIntent::exact([<llc_cpu_0>, <llc_cpu_1>, ...])",
spec,
),
AffinityHint::CrossCgroup { cpus } => map_topology_aware_affinity(
cpus,
"CrossCgroup",
AffinityIntent::CrossCgroup,
"expands to the full topology",
"AffinityIntent::exact([<cpu_0>, <cpu_1>, ...])",
spec,
),
AffinityHint::SmtSiblingPair { cpus } => map_topology_aware_affinity(
cpus,
"SmtSiblingPair",
AffinityIntent::SmtSiblingPair,
"picks an SMT-sibling pair from the cgroup's effective cpuset, \
or the full topology when no cpuset is active",
"AffinityIntent::exact([<sibling_a>, <sibling_b>])",
spec,
),
AffinityHint::Exact { cpus } => {
if cpus.is_empty() {
spec.notes.push(ReproducerNote::UnresolvedAffinity(
"AffinityHint::Exact observed with no CPUs; emitting \
AffinityIntent::Exact(empty) — the spawn-time \
affinity gate rejects an empty Exact set, so this \
spec is NOT runnable as-is. Hand-edit to \
AffinityIntent::exact([<cpu_0>, <cpu_1>, ...]) \
with the observed CPUs, or change to \
AffinityIntent::Inherit."
.into(),
));
} else {
spec.notes
.push(ReproducerNote::Resolved(topology_resolved_note(
"Exact", cpus,
)));
}
AffinityIntent::Exact(cpus_to_set(cpus))
}
AffinityHint::RandomSubset { from, count } => {
if from.is_empty() || *count == 0 {
spec.notes.push(ReproducerNote::UnresolvedAffinity(
"AffinityHint::RandomSubset observed without a \
resolved pool / count; emitting \
AffinityIntent::RandomSubset { from: empty, count: 0 } \
as a placeholder — the spawn-time affinity gate \
rejects empty-pool / zero-count RandomSubset, so \
this spec is NOT runnable as-is. Hand-edit `from` \
to the actual CPU pool and `count` to the desired \
sample size before running, or change to \
AffinityIntent::Inherit."
.into(),
));
AffinityIntent::RandomSubset {
from: BTreeSet::new(),
count: 0,
}
} else {
spec.notes.push(ReproducerNote::Resolved(format!(
"AffinityHint::RandomSubset observed with resolved \
pool {from:?} count={count}; emitting \
AffinityIntent::RandomSubset directly so the \
spawn-time affinity gate accepts it without \
hand-editing",
)));
AffinityIntent::RandomSubset {
from: cpus_to_set(from),
count: *count as usize,
}
}
}
};
push_extras_note(
&mut spec.notes,
"additional affinity hints not modeled",
fp.affinity_hints.iter().skip(1).map(|a| format!("{a:?}")),
);
}
fn map_work_type(fp: &WorkloadFingerprint, spec: &mut ReproducerSpec) {
let Some(primary) = fp.work_type_hints.first() else {
spec.notes.push(ReproducerNote::Informational(
"no work-type hint in fingerprint — defaulting to \
WorkType::SpinWait"
.into(),
));
return;
};
let work_type = match primary {
WorkTypeHint::SpinWait => WorkType::SpinWait,
WorkTypeHint::YieldHeavy => WorkType::YieldHeavy,
WorkTypeHint::Mixed => WorkType::Mixed,
WorkTypeHint::Bursty {
burst_duration,
sleep_duration,
} => WorkType::Bursty {
burst_duration: *burst_duration,
sleep_duration: *sleep_duration,
},
WorkTypeHint::PipeIo => WorkType::PipeIo { burst_iters: 1024 },
WorkTypeHint::FutexPingPong => WorkType::FutexPingPong { spin_iters: 1024 },
WorkTypeHint::CachePressure { size_kib, stride } => WorkType::CachePressure {
size_kib: *size_kib as usize,
stride: *stride as usize,
},
WorkTypeHint::IoSyncWrite => WorkType::IoSyncWrite,
WorkTypeHint::IoRandRead => WorkType::IoRandRead,
WorkTypeHint::IoConvoy => WorkType::IoConvoy,
};
record_work_type(work_type, spec);
push_extras_note(
&mut spec.notes,
"additional work-type hints observed",
fp.work_type_hints.iter().skip(1).map(|w| format!("{w:?}")),
);
}
fn record_work_type(work_type: WorkType, spec: &mut ReproducerSpec) {
spec.config.work_type = work_type;
if is_unmapped_work_type(&spec.config.work_type) {
spec.notes.push(ReproducerNote::UnmappedWorkType(format!(
"no fingerprint mapping for WorkType::{:?} — \
render_run_file_source emits a TODO-decorated \
SpinWait placeholder; hand-edit the rendered source to \
a real builder call before running",
spec.config.work_type,
)));
}
}
fn is_unmapped_work_type(w: &WorkType) -> bool {
match w {
WorkType::SpinWait
| WorkType::YieldHeavy
| WorkType::Mixed
| WorkType::IoSyncWrite
| WorkType::IoRandRead
| WorkType::IoConvoy
| WorkType::Bursty { .. }
| WorkType::PipeIo { .. }
| WorkType::FutexPingPong { .. }
| WorkType::CachePressure { .. } => false,
WorkType::CacheYield { .. }
| WorkType::CachePipe { .. }
| WorkType::FutexFanOut { .. }
| WorkType::Sequence { .. }
| WorkType::ForkExit
| WorkType::NiceSweep
| WorkType::AffinityChurn { .. }
| WorkType::PolicyChurn { .. }
| WorkType::FanOutCompute { .. }
| WorkType::PageFaultChurn { .. }
| WorkType::MutexContention { .. }
| WorkType::Custom { .. }
| WorkType::ThunderingHerd { .. }
| WorkType::PriorityInversion { .. }
| WorkType::ProducerConsumerImbalance { .. }
| WorkType::RtStarvation { .. }
| WorkType::AsymmetricWaker { .. }
| WorkType::WakeChain { .. }
| WorkType::NumaWorkingSetSweep { .. }
| WorkType::CgroupChurn { .. }
| WorkType::SignalStorm { .. }
| WorkType::PreemptStorm { .. }
| WorkType::EpollStorm { .. }
| WorkType::NumaMigrationChurn { .. }
| WorkType::IdleChurn { .. }
| WorkType::AluHot { .. }
| WorkType::SmtSiblingSpin
| WorkType::IpcVariance { .. } => true,
}
}
fn map_sched_policy(fp: &WorkloadFingerprint, spec: &mut ReproducerSpec) {
let Some(primary) = fp.sched_policy_hints.first() else {
return;
};
match primary {
SchedPolicyHint::Other { nice } => {
spec.config.sched_policy = SchedPolicy::Normal;
spec.config.nice = Some(*nice);
}
SchedPolicyHint::Fifo { priority } => {
spec.config.sched_policy = SchedPolicy::Fifo(*priority);
}
SchedPolicyHint::RoundRobin { priority } => {
spec.config.sched_policy = SchedPolicy::RoundRobin(*priority);
}
SchedPolicyHint::Deadline {
runtime_ns,
deadline_ns,
period_ns,
} => {
spec.config.sched_policy = SchedPolicy::deadline(
Duration::from_nanos(*runtime_ns),
Duration::from_nanos(*deadline_ns),
Duration::from_nanos(*period_ns),
);
}
SchedPolicyHint::Batch => spec.config.sched_policy = SchedPolicy::Batch,
SchedPolicyHint::Idle => spec.config.sched_policy = SchedPolicy::Idle,
SchedPolicyHint::Ext => {
spec.notes.push(ReproducerNote::Informational(
"SchedPolicyHint::Ext observed; framework defaults to \
scx routing — no policy override emitted"
.into(),
));
}
}
push_extras_note(
&mut spec.notes,
"additional sched-policy hints observed",
fp.sched_policy_hints
.iter()
.skip(1)
.map(|s| format!("{s:?}")),
);
}
#[allow(dead_code)]
pub fn render_run_file_source(spec: &ReproducerSpec, template_name: &str) -> String {
let mut s = String::new();
s.push_str("// Auto-generated reproducer from a debug capture.\n");
s.push_str("// Edit the WorkloadConfig builder calls to refine\n");
s.push_str("// the projection.\n\n");
if !spec.scheduler_name.is_empty() {
s.push_str(&format!("// Scheduler: {}\n", spec.scheduler_name));
}
if !spec.notes.is_empty() {
s.push_str("//\n// Generator notes:\n");
for note in &spec.notes {
s.push_str(&format!("// - {note}\n"));
}
s.push('\n');
}
s.push_str("use ktstr::workload::*;\n");
s.push_str("use std::collections::BTreeSet;\n");
s.push_str("use std::time::Duration;\n\n");
s.push_str(&format!("pub fn {template_name}() -> WorkloadConfig {{\n"));
s.push_str(" WorkloadConfig::default()\n");
s.push_str(&format!(" .workers({})\n", spec.config.num_workers));
s.push_str(&format!(
" .affinity({})\n",
render_affinity(&spec.config.affinity)
));
s.push_str(&format!(
" .work_type({})\n",
render_work_type(&spec.config.work_type)
));
s.push_str(&format!(
" .sched_policy({})\n",
render_sched_policy(&spec.config.sched_policy)
));
if let Some(n) = spec.config.nice {
s.push_str(&format!(" .nice({n})\n"));
}
s.push_str("}\n");
if !spec.cgroup_hints.is_empty() {
s.push_str("\n// Cgroup hints — apply at harness setup:\n");
for h in &spec.cgroup_hints {
s.push_str(&format!(
"// {} (weight={:?}, mem_max={:?}, cpuset={:?}, cpu_max_quota_us={:?})\n",
h.path, h.cpu_weight, h.memory_max_bytes, h.cpuset_cpus, h.cpu_max_quota_us,
));
}
}
s
}
#[allow(dead_code)]
pub fn render_ktstr_test_source(spec: &ReproducerSpec, template_name: &str) -> String {
let body = render_run_file_source(spec, template_name);
body.replace(
&format!("pub fn {template_name}"),
&format!("#[ktstr::ktstr_test]\npub fn {template_name}"),
)
}
fn render_affinity(a: &AffinityIntent) -> String {
match a {
AffinityIntent::Inherit => "AffinityIntent::Inherit".into(),
AffinityIntent::SingleCpu => "AffinityIntent::SingleCpu".into(),
AffinityIntent::LlcAligned => "AffinityIntent::LlcAligned".into(),
AffinityIntent::CrossCgroup => "AffinityIntent::CrossCgroup".into(),
AffinityIntent::SmtSiblingPair => "AffinityIntent::SmtSiblingPair".into(),
AffinityIntent::RandomSubset { from, count } => {
let cpus: Vec<String> = from.iter().map(|c| c.to_string()).collect();
format!(
"AffinityIntent::RandomSubset {{ from: BTreeSet::from([{}]), count: {} }}",
cpus.join(", "),
count
)
}
AffinityIntent::Exact(set) => {
let cpus: Vec<String> = set.iter().map(|c| c.to_string()).collect();
format!(
"AffinityIntent::Exact(BTreeSet::from([{}]))",
cpus.join(", ")
)
}
}
}
fn render_work_type(w: &WorkType) -> String {
match w {
WorkType::SpinWait => "WorkType::SpinWait".into(),
WorkType::YieldHeavy => "WorkType::YieldHeavy".into(),
WorkType::Mixed => "WorkType::Mixed".into(),
WorkType::IoSyncWrite => "WorkType::IoSyncWrite".into(),
WorkType::IoRandRead => "WorkType::IoRandRead".into(),
WorkType::IoConvoy => "WorkType::IoConvoy".into(),
WorkType::Bursty {
burst_duration,
sleep_duration,
} => format!(
"WorkType::Bursty {{ \
burst_duration: Duration::from_millis({}), \
sleep_duration: Duration::from_millis({}) \
}}",
burst_duration.as_millis(),
sleep_duration.as_millis(),
),
WorkType::PipeIo { burst_iters } => {
format!("WorkType::PipeIo {{ burst_iters: {burst_iters} }}")
}
WorkType::FutexPingPong { spin_iters } => {
format!("WorkType::FutexPingPong {{ spin_iters: {spin_iters} }}")
}
WorkType::CachePressure { size_kib, stride } => {
format!("WorkType::CachePressure {{ size_kib: {size_kib}, stride: {stride} }}")
}
WorkType::CacheYield { .. } => render_work_type_todo("CacheYield"),
WorkType::CachePipe { .. } => render_work_type_todo("CachePipe"),
WorkType::FutexFanOut { .. } => render_work_type_todo("FutexFanOut"),
WorkType::Sequence { .. } => render_work_type_todo("Sequence"),
WorkType::ForkExit => render_work_type_todo("ForkExit"),
WorkType::NiceSweep => render_work_type_todo("NiceSweep"),
WorkType::AffinityChurn { .. } => render_work_type_todo("AffinityChurn"),
WorkType::PolicyChurn { .. } => render_work_type_todo("PolicyChurn"),
WorkType::FanOutCompute { .. } => render_work_type_todo("FanOutCompute"),
WorkType::PageFaultChurn { .. } => render_work_type_todo("PageFaultChurn"),
WorkType::MutexContention { .. } => render_work_type_todo("MutexContention"),
WorkType::Custom { .. } => render_work_type_todo("Custom"),
WorkType::ThunderingHerd { .. } => render_work_type_todo("ThunderingHerd"),
WorkType::PriorityInversion { .. } => render_work_type_todo("PriorityInversion"),
WorkType::ProducerConsumerImbalance { .. } => {
render_work_type_todo("ProducerConsumerImbalance")
}
WorkType::RtStarvation { .. } => render_work_type_todo("RtStarvation"),
WorkType::AsymmetricWaker { .. } => render_work_type_todo("AsymmetricWaker"),
WorkType::WakeChain { .. } => render_work_type_todo("WakeChain"),
WorkType::NumaWorkingSetSweep { .. } => render_work_type_todo("NumaWorkingSetSweep"),
WorkType::CgroupChurn { .. } => render_work_type_todo("CgroupChurn"),
WorkType::SignalStorm { .. } => render_work_type_todo("SignalStorm"),
WorkType::PreemptStorm { .. } => render_work_type_todo("PreemptStorm"),
WorkType::EpollStorm { .. } => render_work_type_todo("EpollStorm"),
WorkType::NumaMigrationChurn { .. } => render_work_type_todo("NumaMigrationChurn"),
WorkType::IdleChurn { .. } => render_work_type_todo("IdleChurn"),
WorkType::AluHot { .. } => render_work_type_todo("AluHot"),
WorkType::SmtSiblingSpin => render_work_type_todo("SmtSiblingSpin"),
WorkType::IpcVariance { .. } => render_work_type_todo("IpcVariance"),
}
}
fn render_work_type_todo(variant: &str) -> String {
format!(
"WorkType::SpinWait /* TODO: no fingerprint mapping for \
WorkType::{variant} — refine from capture */"
)
}
fn render_sched_policy(p: &SchedPolicy) -> String {
match p {
SchedPolicy::Normal => "SchedPolicy::Normal".into(),
SchedPolicy::Batch => "SchedPolicy::Batch".into(),
SchedPolicy::Idle => "SchedPolicy::Idle".into(),
SchedPolicy::Fifo(prio) => format!("SchedPolicy::Fifo({prio})"),
SchedPolicy::RoundRobin(prio) => format!("SchedPolicy::RoundRobin({prio})"),
SchedPolicy::Deadline {
runtime,
deadline,
period,
} => format!(
"SchedPolicy::Deadline {{ runtime: Duration::from_nanos({}), \
deadline: Duration::from_nanos({}), period: Duration::from_nanos({}) }}",
runtime.as_nanos(),
deadline.as_nanos(),
period.as_nanos(),
),
}
}
#[cfg(test)]
#[path = "reproducer_gen_tests.rs"]
mod tests;