use std::collections::HashMap;
use trusty_common::memory_pressure::{MemoryPressure, PressureThresholds, read_memory_pressure};
use super::admission::Readings;
use super::config::BuildLeaseConfig;
use super::slots::HolderRecord;
use crate::core::build_probe::{
BuildGroup, ProcessSampler, ProcessSnapshot, SysinfoSampler, build_groups, is_compiler_process,
};
const MAX_HOPS: usize = 64;
#[must_use]
pub fn foreign_groups(rows: &[ProcessSnapshot], holders: &[HolderRecord]) -> Vec<BuildGroup> {
let leased: Vec<u32> = holders
.iter()
.flat_map(|h| [Some(h.pid), h.child_pid])
.flatten()
.filter(|pid| *pid != 0)
.collect();
foreign_groups_excluding(rows, &leased)
}
fn foreign_groups_excluding(rows: &[ProcessSnapshot], leased: &[u32]) -> Vec<BuildGroup> {
if leased.is_empty() {
return build_groups(rows);
}
let parent_of: HashMap<u32, Option<u32>> = rows.iter().map(|r| (r.pid, r.parent)).collect();
let under_a_lease = |pid: u32| {
let mut cursor = Some(pid);
for _ in 0..MAX_HOPS {
let Some(current) = cursor else {
return false;
};
if leased.contains(¤t) {
return true;
}
cursor = parent_of.get(¤t).copied().flatten();
}
false
};
let kept: Vec<ProcessSnapshot> = rows
.iter()
.filter(|r| !(is_compiler_process(r) && under_a_lease(r.pid)))
.cloned()
.collect();
build_groups(&kept)
}
mod sealed {
pub trait Sealed {}
impl Sealed for super::FixedSampler {}
impl Sealed for super::LiveSampler {}
}
pub trait Sampler: sealed::Sealed {
fn sample(&mut self, holders: &[HolderRecord]) -> Readings;
fn sample_unleased(&mut self) -> Readings {
self.sample(&[])
}
}
#[derive(Debug, Clone)]
pub struct FixedSampler {
readings: Readings,
unleased: Option<Readings>,
}
impl FixedSampler {
#[must_use]
pub fn new(readings: Readings) -> Self {
Self {
readings,
unleased: None,
}
}
#[must_use]
pub fn with_unleased(mut self, unleased: Readings) -> Self {
self.unleased = Some(unleased);
self
}
}
impl Sampler for FixedSampler {
fn sample(&mut self, _holders: &[HolderRecord]) -> Readings {
self.readings.clone()
}
fn sample_unleased(&mut self) -> Readings {
self.unleased
.clone()
.unwrap_or_else(|| self.readings.clone())
}
}
pub struct LiveSampler {
processes: SysinfoSampler,
thresholds: PressureThresholds,
host: String,
}
impl LiveSampler {
#[must_use]
pub fn new(config: &BuildLeaseConfig) -> Self {
Self {
processes: SysinfoSampler::new(),
thresholds: PressureThresholds::default()
.with_min_available_pct(config.effective_min_available_pct()),
host: host_name(),
}
}
fn pressure_and_load(&self) -> (Result<MemoryPressure, String>, Result<f64, String>) {
let pressure = read_memory_pressure(&self.thresholds).map_err(|err| match err.errno() {
Some(errno) => format!("{err} (errno {errno})"),
None => err.to_string(),
});
let load = trusty_common::load_average::read_load_average()
.map(|avg| avg.one_minute)
.map_err(|err| err.to_string());
(pressure, load)
}
}
impl Sampler for LiveSampler {
fn sample(&mut self, holders: &[HolderRecord]) -> Readings {
let (pressure, load) = self.pressure_and_load();
let foreign = self
.processes
.sample()
.map(|rows| foreign_groups(&rows, holders))
.map_err(|err| err.to_string());
Readings::new(pressure, load, logical_cores(), foreign, self.host.clone())
}
fn sample_unleased(&mut self) -> Readings {
let (pressure, load) = self.pressure_and_load();
let foreign = self
.processes
.sample()
.map_err(|err| err.to_string())
.and_then(|rows| {
let lease = lease_processes()?;
let mut groups = foreign_groups_excluding(&rows, &lease.all);
groups.extend(lease.earlier.iter().map(|pid| unleased_group(*pid)));
Ok(groups)
});
Readings::new(pressure, load, logical_cores(), foreign, self.host.clone())
}
}
fn logical_cores() -> usize {
std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get)
}
struct LeaseProcesses {
all: Vec<u32>,
earlier: Vec<u32>,
}
fn lease_processes() -> Result<LeaseProcesses, String> {
use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};
let mut sys = System::new();
sys.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing().with_cmd(UpdateKind::Always),
);
let me = std::process::id();
let my_start = sys
.process(Pid::from_u32(me))
.map(sysinfo::Process::start_time)
.ok_or_else(|| "this process is missing from the process table".to_string())?;
let mut all = Vec::new();
let mut earlier = Vec::new();
for (pid, proc_) in sys.processes() {
let pid = pid.as_u32();
if pid == me || !proc_.cmd().iter().any(|a| a == "build-lease") {
continue;
}
let program = proc_
.cmd()
.first()
.map(|a| a.to_string_lossy().into_owned());
let base = program
.as_deref()
.and_then(|p| p.rsplit('/').next())
.unwrap_or("");
if base != "tm" && base != "trusty-mpm" {
continue;
}
all.push(pid);
if (proc_.start_time(), pid) < (my_start, me) {
earlier.push(pid);
}
}
Ok(LeaseProcesses { all, earlier })
}
fn unleased_group(pid: u32) -> BuildGroup {
BuildGroup {
root_pid: pid,
root_name: "tm build-lease".to_string(),
compilers: vec!["queued or running without a lease".to_string()],
cpu_pct: 0.0,
ancestry: vec![pid],
}
}
#[must_use]
pub fn host_name() -> String {
let mut buf = [0u8; 256];
let rc = unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) };
if rc != 0 {
return "unknown-host".to_string();
}
buf.iter()
.position(|b| *b == 0)
.and_then(|end| String::from_utf8(buf[..end].to_vec()).ok())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown-host".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn row(pid: u32, parent: Option<u32>, name: &str) -> ProcessSnapshot {
ProcessSnapshot {
pid,
parent,
name: name.into(),
exe: None,
cpu_pct: 50.0,
}
}
fn holder(pid: u32, child: Option<u32>) -> HolderRecord {
HolderRecord {
slot: 0,
pid,
child_pid: child,
command: "cargo test".into(),
cwd: "/repo".into(),
started_at: String::new(),
target_dir: None,
}
}
#[test]
fn a_leased_build_is_not_foreign() {
let table = vec![
row(1, None, "zsh"),
row(20, Some(1), "tm"),
row(21, Some(20), "cargo"),
row(22, Some(21), "rustc"),
];
assert!(foreign_groups(&table, &[holder(20, Some(21))]).is_empty());
assert_eq!(
foreign_groups(&table, &[holder(0, None)]).len(),
1,
"pid 0 excludes nothing"
);
}
#[test]
fn a_terminal_cargo_is_foreign() {
let table = vec![
row(1, None, "zsh"),
row(20, Some(1), "tm"),
row(21, Some(20), "cargo"),
row(22, Some(21), "rustc"),
row(30, Some(1), "cargo"),
row(31, Some(30), "rustc"),
];
let foreign = foreign_groups(&table, &[holder(20, Some(21))]);
assert_eq!(foreign.len(), 1);
assert_eq!(foreign[0].root_pid, 30);
}
#[test]
fn a_driver_above_the_lease_does_not_make_it_foreign() {
let table = vec![
row(10, None, "make"),
row(20, Some(10), "tm"),
row(21, Some(20), "cargo"),
row(22, Some(21), "rustc"),
];
assert!(foreign_groups(&table, &[holder(20, None)]).is_empty());
}
#[test]
#[serial_test::serial(build_slot_fds)] fn a_real_foreign_compiler_is_counted() {
let tmp = tempfile::tempdir().expect("tempdir");
let fake = tmp.path().join("rustc");
std::os::unix::fs::symlink("/bin/sleep", &fake).expect("symlink");
let mut child = std::process::Command::new(&fake)
.arg("30")
.spawn()
.expect("spawn the fake compiler");
let sampler = SysinfoSampler::new();
let rows = sampler.sample().expect("process table");
let _ = child.kill();
let _ = child.wait();
assert!(
rows.iter()
.any(|r| r.pid == child.id() && is_compiler_process(r)),
"the spawned `rustc` must read as a compiler: {:?}",
rows.iter().find(|r| r.pid == child.id())
);
let count = |groups: Vec<BuildGroup>| -> usize {
groups
.iter()
.map(|g| g.compilers.iter().filter(|n| n.as_str() == "rustc").count())
.sum()
};
let unleased = count(foreign_groups(&rows, &[]));
let leased = count(foreign_groups(&rows, &[holder(std::process::id(), None)]));
assert_eq!(
unleased,
leased + 1,
"exactly this test's rustc is excluded by the lease"
);
}
#[test]
fn live_readings_are_plausible_on_this_host() {
let mut sampler = LiveSampler::new(&BuildLeaseConfig::default());
let r = sampler.sample(&[]);
assert!(r.logical_cores >= 1);
assert!(!r.host.is_empty());
assert!(r.foreign.is_ok(), "{:?}", r.foreign);
let u = sampler.sample_unleased();
assert!(u.foreign.is_ok(), "{:?}", u.foreign);
}
#[test]
fn a_fixed_sampler_replays_its_readings() {
let readings = Readings::new(
Err("no pressure".into()),
Ok(1.5),
4,
Ok(Vec::new()),
"fixed-host",
);
let mut sampler = FixedSampler::new(readings);
for _ in 0..2 {
let r = sampler.sample(&[]);
assert_eq!(r.host, "fixed-host");
assert_eq!(r.load_avg_1min, Ok(1.5));
}
}
}