use super::plan::Processor;
use super::{MODULE, notice};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Did {
#[cfg_attr(
not(windows),
expect(
dead_code,
reason = "only Windows takes the process into a job whose affinity limit this is"
)
)]
Pinned {
group: u16,
mask: u64,
runs: runtime::backlog::RunAffinity,
},
#[cfg_attr(
not(target_os = "linux"),
expect(dead_code, reason = "only Linux answers with a taskset list")
)]
External { list: String },
Nothing { reason: String },
}
static TAKEN: std::sync::OnceLock<Did> = std::sync::OnceLock::new();
#[cfg(windows)]
static JOB: std::sync::OnceLock<win32job::Job> = std::sync::OnceLock::new();
pub(crate) fn take(seats: &[Processor]) -> Did {
let did = TAKEN.get_or_init(|| acted(seats)).clone();
if !matches!(did, Did::Pinned { .. }) {
notice(
MODULE,
&format!(
"[core] placement = \"pinned\": {}",
words(&did, seats.len())
),
);
}
did
}
pub(crate) fn run_affinity() -> runtime::backlog::RunAffinity {
match TAKEN.get() {
Some(Did::Pinned { runs, .. }) => *runs,
Some(Did::External { .. } | Did::Nothing { .. }) | None => {
runtime::backlog::RunAffinity::Os
}
}
}
pub(crate) fn clause(seats: &[Processor]) -> String {
let did = match TAKEN.get() {
Some(did) => did.clone(),
None => would(seats),
};
words(&did, seats.len())
}
fn words(did: &Did, cores: usize) -> String {
match did {
Did::Pinned { .. } => format!(
"hard affinity: the process is held to the {cores} planned cores by a Job Object"
),
Did::External { list } => format!(
"hard affinity: start the binary under `taskset -c {list}`; this arm sets nothing inside the process"
),
Did::Nothing { reason } => format!("hard affinity: nothing is pinned ({reason})"),
}
}
#[cfg(windows)]
fn acted(seats: &[Processor]) -> Did {
let (group, mask, runs) = match would(seats) {
Did::Pinned { group, mask, runs } => (group, mask, runs),
external @ Did::External { .. } | external @ Did::Nothing { .. } => return external,
};
let Ok(wide) = usize::try_from(mask) else {
return Did::Nothing {
reason: "the plan's mask does not fit this build's word".to_owned(),
};
};
let mut info = win32job::ExtendedLimitInfo::default();
info.limit_affinity(wide).limit_silent_breakaway_ok();
let job = match win32job::Job::create_with_limit_info(&info) {
Ok(job) => job,
Err(err) => {
return Did::Nothing {
reason: format!("a job with that affinity limit was refused ({err})"),
};
}
};
if let Err(err) = job.assign_current_process() {
return Did::Nothing {
reason: format!("this process could not join the job ({err})"),
};
}
if JOB.set(job).is_err() {
return Did::Nothing {
reason: "the process was already taken".to_owned(),
};
}
if runs == runtime::backlog::RunAffinity::Os {
notice(
MODULE,
"[core] placement = \"pinned\": no remaining processors for run affinity",
);
}
Did::Pinned { group, mask, runs }
}
#[cfg(not(windows))]
fn acted(seats: &[Processor]) -> Did {
would(seats)
}
fn would(seats: &[Processor]) -> Did {
if seats.is_empty() {
return Did::Nothing {
reason: "the plan leaves every processor to the operating system".to_owned(),
};
}
#[cfg(windows)]
{
let group = match desktop_ffi::cpu::thread_group() {
Ok(group) => group,
Err(err) => {
return Did::Nothing {
reason: format!("this thread's processor group is unreadable ({err:?})"),
};
}
};
let mask = seats
.iter()
.filter(|seat| seat.group == group.group)
.filter_map(|seat| 1_u64.checked_shl(seat.number))
.fold(0, |mask, bit| mask | bit)
& group.mask;
if mask == 0 {
return Did::Nothing {
reason: "the plan names no processor of this thread's group".to_owned(),
};
}
Did::Pinned {
group: group.group,
mask,
runs: remaining(group.mask, mask),
}
}
#[cfg(target_os = "linux")]
{
Did::External {
list: seats
.iter()
.map(|seat| seat.number.to_string())
.collect::<Vec<_>>()
.join(","),
}
}
#[cfg(not(any(windows, target_os = "linux")))]
{
Did::Nothing {
reason: "this platform has no hard-affinity call".to_owned(),
}
}
}
#[cfg(any(windows, test))]
fn remaining(available: u64, core: u64) -> runtime::backlog::RunAffinity {
match usize::try_from(available & !core) {
Ok(mask) => match std::num::NonZeroUsize::new(mask) {
Some(mask) => runtime::backlog::RunAffinity::Mask(mask),
None => runtime::backlog::RunAffinity::Os,
},
Err(_beyond) => runtime::backlog::RunAffinity::Os,
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "test code"
)]
mod tests {
use super::*;
#[cfg(windows)]
#[test]
#[expect(clippy::disallowed_methods, reason = "test fixture (child D4)")]
fn the_job_reports_the_mask_the_arm_asked_for() {
let group = desktop_ffi::cpu::thread_group().unwrap();
let seats: Vec<Processor> = (0..64)
.filter(|&number| group.mask >> number & 1 == 1)
.take(2)
.map(|number| Processor {
group: group.group,
number,
})
.collect();
assert_eq!(seats.len(), 2, "this machine lends two processors");
let did = take(&seats);
let Did::Pinned { mask, runs, .. } = did else {
panic!("the arm pinned nothing: {did:?}");
};
assert_eq!(desktop_ffi::cpu::thread_group().unwrap().mask, mask);
assert_eq!(run_affinity(), runs);
let runtime::backlog::RunAffinity::Mask(run_mask) = runs else {
panic!("this machine lends processors outside the two core seats");
};
let backlog =
runtime::Backlog::with_window(runtime::PollBudget::new(1, 1)).with_affinity(runs);
let scratch = tempfile::tempdir().unwrap();
let gate = scratch.path().join("gate");
let report = scratch.path().join("affinity");
let mut child = std::process::Command::new("powershell.exe");
child.args([
"-NoProfile",
"-NonInteractive",
"-Command",
"while (-not (Test-Path -LiteralPath $env:R05_GATE)) { Start-Sleep -Milliseconds 20 }; \
[IO.File]::WriteAllText($env:R05_REPORT + '.pending', \
[Diagnostics.Process]::GetCurrentProcess().ProcessorAffinity.ToInt64().ToString()); \
[IO.File]::Move($env:R05_REPORT + '.pending', $env:R05_REPORT)",
]);
child
.env("R05_GATE", &gate)
.env("R05_REPORT", &report)
.current_dir(std::env::temp_dir());
let owner = kernel::RunId::from_bytes([10; 16]);
let scope = kernel::Address::parse("vault/room1").unwrap();
let started = backlog.run(owner, &scope, "remaining processors".to_owned(), child);
let polls = 3000;
let interval = std::time::Duration::from_millis(20);
let observed = (|| -> std::io::Result<(String, runtime::Finished)> {
if let Err(err) = &started {
return Err(std::io::Error::other(format!(
"start affinity reader: {err}"
)));
}
std::fs::write(&gate, b"ready")?;
let mut observed = None;
for _ in 0..polls {
match std::fs::read_to_string(&report) {
Ok(value) => {
observed = Some(value);
break;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err),
}
std::thread::sleep(interval);
}
let observed = observed.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("affinity reader published no report after {polls} polls: {started:?}"),
)
})?;
for _ in 0..polls {
if let Some(finished) = backlog
.harvest(owner)
.map_err(|err| std::io::Error::other(err.to_string()))?
.into_iter()
.next()
{
return Ok((observed, finished));
}
std::thread::sleep(interval);
}
Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("affinity reader did not exit after {polls} polls"),
))
})();
backlog.release(owner);
let reaped = (|| -> Result<(), String> {
for _ in 0..polls {
backlog.harvest(owner).map_err(|err| err.to_string())?;
if backlog
.standing(&scope)
.map_err(|err| err.to_string())?
.is_empty()
{
return Ok(());
}
std::thread::sleep(interval);
}
Err(format!(
"released affinity reader remains in the backlog after {polls} polls"
))
})();
let removed = scratch.close();
reaped.expect("release and harvest the affinity reader");
removed.expect("remove the affinity reader's gate and report files");
let (observed, finished) =
observed.expect("read the child process's affinity report and exit");
assert_eq!(
finished.exit,
runtime::Exit::Ended { code: 0 },
"{}",
finished.stderr
);
assert_eq!(observed.trim().parse::<usize>().unwrap(), run_mask.get());
assert_eq!(u64::try_from(run_mask.get()).unwrap() & mask, 0);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_asks_to_be_started_under_taskset() {
let seats = vec![
Processor {
group: 0,
number: 2,
},
Processor {
group: 0,
number: 4,
},
];
assert_eq!(
would(&seats),
Did::External {
list: "2,4".to_owned()
}
);
}
#[cfg(not(any(windows, target_os = "linux")))]
#[test]
fn a_platform_without_the_call_says_so() {
let seats = vec![Processor {
group: 0,
number: 0,
}];
let Did::Nothing { reason } = would(&seats) else {
panic!("this platform is expected to have no hard-affinity call");
};
assert!(reason.contains("no hard-affinity call"), "{reason}");
assert!(clause(&seats).contains("nothing is pinned"));
}
#[test]
fn run_processors_are_available_and_outside_the_core_plan() {
for available in 0_u64..256 {
for core in 0_u64..256 {
match remaining(available, core) {
runtime::backlog::RunAffinity::Os => assert_eq!(available & !core, 0),
runtime::backlog::RunAffinity::Mask(mask) => {
let runs = u64::try_from(mask.get()).unwrap();
assert_eq!(runs & core, 0);
assert_eq!(runs & !available, 0);
assert_eq!(runs | (available & core), available);
}
}
}
}
}
#[test]
fn an_empty_plan_pins_nothing_and_says_so() {
let Did::Nothing { reason } = would(&[]) else {
panic!("an empty plan asks nothing of any platform");
};
assert!(reason.contains("leaves every processor"), "{reason}");
}
}