use crate::config::{Config, SchedulerPriority};
use eyre::{Context, Result};
use log::debug;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
const SCHEDULER_DIR: &str = "scheduler";
const LEASES_DIR: &str = "leases";
const INFLIGHT_DIR: &str = "inflight";
const POOL_LOCK: &str = "pool.lock";
const PRIORITY_WAIT_STAMP: &str = "priority-wait";
const LEDGER_FILE: &str = "memory.json";
const LEDGER_LOCK: &str = "memory.lock";
const TEST_HISTORY_DIR: &str = "tests";
const TEST_HISTORY_MAGIC: &[u8; 8] = b"mbxtest\x01";
const TEST_HISTORY_HEADER_LEN: u64 = 32;
const TEST_HISTORY_RECORD_LEN: u64 = 24;
const TEST_HISTORY_LOCK: &str = "tests.lock";
pub(crate) const SCHED_DIR_ENV: &str = "MBX_SCHED_DIR";
pub(crate) const SCHED_DISABLE_ENV: &str = "MBX_SCHED_DISABLE";
pub(crate) const SCHED_SLOTS_ENV: &str = "MBX_SCHED_SLOTS";
pub(crate) const SCHED_SLOT_BYTES_ENV: &str = "MBX_SCHED_SLOT_BYTES";
pub(crate) const SCHED_PRIORITY_ENV: &str = "MBX_SCHED_PRIORITY";
const SCHED_PRESSURE_ENV: &str = "MBX_SCHED_PRESSURE";
const SCHED_BUILD_ID_ENV: &str = "MBX_SCHED_BUILD_ID";
const SCHED_BUILD_SLOTS_ENV: &str = "MBX_SCHED_BUILD_SLOTS";
const LEASE_VERSION: u8 = 1;
const FLIGHT_VERSION: u8 = 1;
const MAX_FLIGHT_PAYLOAD: usize = 4 * 1024 * 1024;
const LEDGER_VERSION: u8 = 1;
const MAX_LEDGER_ENTRIES: usize = 1024;
const MAX_LINK_SAMPLES: usize = 16;
const MIN_LINK_SAMPLES: usize = 3;
const LINK_WEIGHT: u64 = 2;
const UNMEASURED_TEST_WEIGHT: u64 = 2;
const TEST_HISTORY_REFRESH: Duration = Duration::from_secs(24 * 60 * 60);
const MAX_TEST_HISTORY_ENTRIES: usize = 16_384;
const POLL_INITIAL: Duration = Duration::from_millis(2);
const POLL_MAX: Duration = Duration::from_millis(25);
const PRIORITY_WAIT_FRESHNESS: Duration = Duration::from_secs(2);
const PRIORITY_STAMP_INTERVAL: Duration = Duration::from_millis(500);
const SLOW_WAIT_NOTICE: Duration = Duration::from_secs(60);
const GATE_DEADLINE: Duration = Duration::from_secs(120);
static LEASE_NONCE: AtomicU64 = AtomicU64::new(0);
fn next_lease_nonce() -> u64 {
LEASE_NONCE.fetch_add(1, Ordering::Relaxed)
}
const LEASE_NAME_ATTEMPTS: usize = 8;
fn process_token() -> &'static str {
static TOKEN: OnceLock<String> = OnceLock::new();
TOKEN.get_or_init(|| crate::util::random_string(8))
}
pub(crate) struct Demand {
name: String,
links: bool,
threads: Option<u64>,
learns_cpu: bool,
profile: Option<String>,
per_test: Option<TestIdentity>,
}
#[derive(Debug, Clone)]
struct TestIdentity {
binary: String,
test: String,
known: std::cell::Cell<Option<TestMeasurement>>,
}
impl Demand {
pub(crate) fn new(name: &str, links: bool) -> Self {
Self {
name: name.to_string(),
links,
threads: Some(1),
learns_cpu: false,
profile: None,
per_test: None,
}
}
pub(crate) fn with_link_profile(mut self, arguments: &[std::ffi::OsString]) -> Self {
if self.links {
let profile = link_profile(arguments);
self.name = format!("{} [{profile}]", self.name);
self.profile = Some(profile);
}
self
}
pub(crate) fn test(name: &str, threads: Option<u64>) -> Self {
Self {
name: match threads {
Some(threads) => format!("{name} [test, {threads} threads]"),
None => format!("{name} [test]"),
},
links: false,
threads,
learns_cpu: true,
profile: None,
per_test: None,
}
}
pub(crate) fn nextest(binary: &str, test: &str) -> Self {
Self {
name: format!("{binary} {test} [nextest]"),
links: false,
threads: Some(1),
learns_cpu: true,
profile: None,
per_test: Some(TestIdentity {
binary: binary.to_owned(),
test: test.to_owned(),
known: std::cell::Cell::new(None),
}),
}
}
}
pub(crate) struct Permit {
lock: Option<fslock::LockFile>,
path: PathBuf,
}
impl Drop for Permit {
fn drop(&mut self) {
drop(self.lock.take());
let _ = std::fs::remove_file(&self.path);
}
}
#[derive(Debug, Serialize, Deserialize)]
struct Lease {
version: u8,
weight: u64,
priority: String,
#[serde(default)]
build_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct FlightRecord {
version: u8,
adapter: String,
invocation: String,
payload: String,
}
pub(crate) struct Flight {
lock: Option<fslock::LockFile>,
path: PathBuf,
adapter: &'static str,
invocation: String,
waited: bool,
inherited: Option<String>,
}
impl Flight {
pub(crate) fn waited(&self) -> bool {
self.waited
}
pub(crate) fn inherited(&self) -> Option<&str> {
self.inherited.as_deref()
}
pub(crate) fn leave(&self, payload: &str) {
if payload.len() > MAX_FLIGHT_PAYLOAD {
return;
}
let record = FlightRecord {
version: FLIGHT_VERSION,
adapter: self.adapter.into(),
invocation: self.invocation.clone(),
payload: payload.into(),
};
let written = serde_json::to_vec(&record)
.map_err(eyre::Report::from)
.and_then(|bytes| crate::util::write_advisory(&record_path(&self.path), &bytes));
if let Err(error) = written {
debug!("a flight prediction was not left behind: {error:#}");
}
}
}
fn record_path(lock_path: &Path) -> PathBuf {
let mut path = lock_path.as_os_str().to_owned();
path.push(".prediction");
PathBuf::from(path)
}
const FLIGHT_MAX_AGE: Duration = Duration::from_secs(30 * 24 * 60 * 60);
pub(crate) fn prune_flights(cache_dir: &Path) {
let dir = cache_dir.join(SCHEDULER_DIR).join(INFLIGHT_DIR);
let Ok(entries) = std::fs::read_dir(&dir) else {
return;
};
let stale = |path: &Path| {
std::fs::metadata(path)
.and_then(|metadata| metadata.modified())
.ok()
.and_then(|modified| modified.elapsed().ok())
.is_some_and(|age| age > FLIGHT_MAX_AGE)
};
for entry in entries.flatten() {
let path = entry.path();
if path
.extension()
.is_some_and(|extension| extension == "prediction")
{
if stale(&path) && !path.with_extension("").exists() {
let _ = std::fs::remove_file(&path);
}
continue;
}
let record = record_path(&path);
if !stale(&path) || (record.exists() && !stale(&record)) {
continue;
}
let Ok(mut lock) = fslock::LockFile::open(&path) else {
continue;
};
if lock.try_lock().is_ok_and(|taken| taken) {
let _ = std::fs::remove_file(&record);
drop(lock);
let _ = std::fs::remove_file(&path);
}
}
}
pub(crate) fn prune_test_history(cache_dir: &Path) {
let scheduler = cache_dir.join(SCHEDULER_DIR);
let Ok(entries) = std::fs::read_dir(scheduler.join(TEST_HISTORY_DIR)) else {
return;
};
let Ok(mut lock) = fslock::LockFile::open(&scheduler.join(TEST_HISTORY_LOCK)) else {
return;
};
if !lock.try_lock().is_ok_and(|taken| taken) {
return;
}
for entry in entries.flatten() {
if !entry.file_type().is_ok_and(|kind| kind.is_file()) {
continue;
}
let path = entry.path();
let stale = std::fs::metadata(&path)
.and_then(|metadata| metadata.modified())
.ok()
.and_then(|modified| modified.elapsed().ok())
.is_some_and(|age| age > FLIGHT_MAX_AGE);
if stale {
let _ = std::fs::remove_file(&path);
}
}
}
impl Drop for Flight {
fn drop(&mut self) {
drop(self.lock.take());
}
}
pub(crate) fn flight(adapter: &'static str, invocation: &str) -> Option<Flight> {
let pool = pool()?;
match flight_at(&pool.dir, adapter, invocation) {
Ok(flight) => Some(flight),
Err(error) => {
debug!("compiling outside a flight: {error:#}");
None
}
}
}
fn flight_at(pool_dir: &Path, adapter: &'static str, invocation: &str) -> Result<Flight> {
let _phase = crate::phase_timing::phase("flight_wait");
let dir = pool_dir.join(INFLIGHT_DIR);
std::fs::create_dir_all(&dir)
.wrap_err_with(|| format!("failed to create {}", dir.display()))?;
let path = dir.join(format!("{adapter}-{invocation}"));
let mut lock = fslock::LockFile::open(&path)?;
let waited = !lock.try_lock()?;
if waited {
lock.lock()?;
}
let inherited = std::fs::read(record_path(&path))
.ok()
.and_then(|bytes| serde_json::from_slice::<FlightRecord>(&bytes).ok())
.filter(|record| {
record.version == FLIGHT_VERSION
&& record.adapter == adapter
&& record.invocation == invocation
})
.map(|record| record.payload);
Ok(Flight {
lock: Some(lock),
path,
adapter,
invocation: invocation.to_string(),
waited,
inherited,
})
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct MemoryLedger {
version: u8,
crates: BTreeMap<String, u64>,
link_peaks: Vec<u64>,
#[serde(default)]
cpus: BTreeMap<String, u64>,
#[serde(default)]
profile_link_peaks: BTreeMap<String, Vec<u64>>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct TestMeasurement {
peak: Option<u64>,
cores: Option<u64>,
}
#[derive(Debug, Clone, Copy, Default)]
struct TestHistoryHeader {
count: u64,
maximum: TestMeasurement,
}
#[derive(Debug, Clone, Copy, Default)]
struct TestHistoryLookup {
test: Option<TestMeasurement>,
header: TestHistoryHeader,
}
#[derive(Debug, Clone, Copy, Default)]
struct TestHistoryRecord {
hash: u64,
measurement: TestMeasurement,
}
pub(crate) struct Pool {
pub(crate) dir: PathBuf,
capacity: u64,
build: Option<(String, u64)>,
bytes_per_permit: u64,
priority: SchedulerPriority,
available_memory: fn() -> Option<u64>,
pressure: bool,
pressure_probe: fn() -> crate::pressure::Reading,
pressure_domain: &'static str,
clock: fn() -> u64,
lease_nonce: fn() -> u64,
}
pub(crate) fn pool() -> Option<&'static Pool> {
static POOL: OnceLock<Option<Pool>> = OnceLock::new();
POOL.get_or_init(resolve_pool).as_ref()
}
pub(crate) fn reservation_pool(config: &Config, priority: SchedulerPriority) -> Option<Pool> {
let scheduler = &config.scheduler;
scheduler.enabled.then(|| {
let permits = scheduler.permits();
let mut pool = Pool::new(
config.cache_dir.join(SCHEDULER_DIR),
permits,
bytes_per_permit(scheduler.memory_bytes, permits),
priority,
);
pool.pressure = scheduler.pressure;
pool
})
}
fn resolve_pool() -> Option<Pool> {
match std::env::var_os(SCHED_DIR_ENV) {
Some(dir) if dir.is_empty() => None,
Some(dir) => {
let capacity = env_number(SCHED_SLOTS_ENV).unwrap_or_else(default_capacity);
let mut pool = Pool::new(
PathBuf::from(dir),
capacity,
env_number(SCHED_SLOT_BYTES_ENV).unwrap_or(0),
std::env::var(SCHED_PRIORITY_ENV)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or_default(),
);
pool.pressure = env_number(SCHED_PRESSURE_ENV) == Some(1);
pool.build = std::env::var(SCHED_BUILD_ID_ENV)
.ok()
.filter(|id| !id.is_empty())
.map(|id| {
let slots = env_number(SCHED_BUILD_SLOTS_ENV)
.unwrap_or(capacity)
.clamp(1, capacity.max(1));
(id, slots)
});
Some(pool)
}
None => match Config::load() {
Ok(config) => Pool::from_config(&config),
Err(error) => {
debug!("compile scheduling is off; configuration did not load: {error:#}");
None
}
},
}
}
fn env_number(name: &str) -> Option<u64> {
std::env::var(name).ok()?.parse().ok()
}
fn default_capacity() -> u64 {
std::thread::available_parallelism().map_or(1, |cpus| cpus.get() as u64)
}
pub(crate) fn session_environment_with_jobs(
config: &Config,
cargo_jobs: Option<u64>,
) -> Vec<(String, String)> {
let scheduler = &config.scheduler;
if !scheduler.enabled || std::env::var_os(SCHED_DISABLE_ENV).is_some() {
return vec![(SCHED_DIR_ENV.into(), String::new())];
}
let permits = scheduler.permits();
let build_permits = cargo_jobs.map_or(permits, |jobs| permits.min(jobs.max(1)));
vec![
(
SCHED_DIR_ENV.into(),
config
.cache_dir
.join(SCHEDULER_DIR)
.to_string_lossy()
.into_owned(),
),
(SCHED_SLOTS_ENV.into(), permits.to_string()),
(
SCHED_PRESSURE_ENV.into(),
u8::from(scheduler.pressure).to_string(),
),
(
SCHED_SLOT_BYTES_ENV.into(),
bytes_per_permit(scheduler.memory_bytes, permits).to_string(),
),
(
SCHED_PRIORITY_ENV.into(),
scheduler.priority.as_str().into(),
),
(
"MBX_SCHED_SUSPEND".into(),
u8::from(scheduler.suspend && scheduler.pressure && scheduler.memory_bytes.is_some())
.to_string(),
),
(
"MBX_SCHED_CGROUP_ROOT".into(),
scheduler
.cgroup_root
.as_ref()
.map_or_else(String::new, |path| path.to_string_lossy().into_owned()),
),
(SCHED_BUILD_ID_ENV.into(), crate::util::random_string(12)),
(SCHED_BUILD_SLOTS_ENV.into(), build_permits.to_string()),
]
}
fn bytes_per_permit(memory_bytes: Option<u64>, cpus: u64) -> u64 {
memory_bytes.map_or(0, |memory| memory / cpus.max(1))
}
pub(crate) fn record_compiler_memory(demand: &Demand, status: &ExitStatus) {
let Some(pool) = pool() else {
return;
};
pool.note_compilation(demand, status);
}
pub(crate) fn record_test_run(demand: &Demand, status: &ExitStatus, cores: Option<u64>) {
let Some(pool) = pool() else {
return;
};
let Some(identity) = &demand.per_test else {
pool.note_compilation(demand, status);
if let Some(cores) = cores
&& let Err(error) = pool.record_cpu(&demand.name, cores)
{
debug!("test CPU use was not recorded: {error:#}");
}
return;
};
let peak = child_peak_rss_bytes().and_then(|measured| {
ledger_peak(
measured,
oom_killed(status),
true,
status.success(),
pool.bytes_per_permit,
)
});
if let Err(error) = pool.record_test_measurement(identity, peak, cores) {
debug!("test measurements were not recorded: {error:#}");
}
}
pub(crate) fn average_cores(cpu: Duration, wall: Duration) -> u64 {
if wall.is_zero() {
return 1;
}
(cpu.as_secs_f64() / wall.as_secs_f64()).round().max(1.0) as u64
}
impl Pool {
fn new(
dir: PathBuf,
capacity: u64,
bytes_per_permit: u64,
priority: SchedulerPriority,
) -> Self {
Self {
dir,
capacity: capacity.max(1),
build: None,
bytes_per_permit,
priority,
available_memory: crate::util::memory_available_bytes,
pressure: true,
pressure_probe: crate::pressure::probe,
pressure_domain: crate::pressure::domain(),
clock: crate::pressure::now_ms,
lease_nonce: next_lease_nonce,
}
}
fn from_config(config: &Config) -> Option<Self> {
let scheduler = &config.scheduler;
let permits = scheduler.permits();
scheduler.enabled.then(|| {
let mut pool = Self::new(
config.cache_dir.join(SCHEDULER_DIR),
permits,
bytes_per_permit(scheduler.memory_bytes, permits),
scheduler.priority,
);
pool.pressure = scheduler.pressure;
pool
})
}
pub(crate) fn admit(&self, demand: &Demand) -> Option<Permit> {
let _phase = crate::phase_timing::phase("permit_wait");
let (weight, predicted) = self.plan(demand);
let started = Instant::now();
let mut delay = POLL_INITIAL;
let mut stamped: Option<Instant> = None;
let mut complained = false;
loop {
let gate = predicted.filter(|_| started.elapsed() < GATE_DEADLINE);
match self.try_admit(weight, gate) {
Ok(Some(permit)) => return Some(permit),
Ok(None) => {}
Err(error) => {
debug!("compiling without a machine-wide permit: {error:#}");
return None;
}
}
if self.priority == SchedulerPriority::Normal
&& stamped.is_none_or(|last| last.elapsed() >= PRIORITY_STAMP_INTERVAL)
{
let _ = std::fs::write(self.dir.join(PRIORITY_WAIT_STAMP), b"");
stamped = Some(Instant::now());
}
if !complained && started.elapsed() >= SLOW_WAIT_NOTICE {
complained = true;
if self.pressure && self.bytes_per_permit > 0 {
debug!(
"compiler admission may be waiting for memory pressure to recover ({})",
demand.name
);
}
debug!(
"waiting for {weight} of {} machine-wide compile permits ({})",
self.capacity, demand.name
);
}
std::thread::sleep(jittered(delay));
delay = (delay * 2).min(POLL_MAX);
}
}
pub(crate) fn reserve(&self, cpus: Option<u64>, memory_bytes: Option<u64>) -> Result<Permit> {
let cpu_weight = cpus.unwrap_or(0);
let memory_weight = match memory_bytes {
Some(_) if self.bytes_per_permit == 0 => {
eyre::bail!("--memory needs scheduler.memory to be configured")
}
Some(memory) => memory.div_ceil(self.bytes_per_permit),
None => 0,
};
let weight = cpu_weight.max(memory_weight);
if weight == 0 {
eyre::bail!("reserve needs --cpus, --memory, or both")
}
let started = Instant::now();
let mut delay = POLL_INITIAL;
let mut stamped: Option<Instant> = None;
loop {
let gate = memory_bytes.filter(|_| started.elapsed() < GATE_DEADLINE);
if let Some(permit) = self.try_admit(weight, gate)? {
return Ok(permit);
}
if self.priority == SchedulerPriority::Normal
&& stamped.is_none_or(|last| last.elapsed() >= PRIORITY_STAMP_INTERVAL)
{
let _ = std::fs::write(self.dir.join(PRIORITY_WAIT_STAMP), b"");
stamped = Some(Instant::now());
}
std::thread::sleep(jittered(delay));
delay = (delay * 2).min(POLL_MAX);
}
}
fn plan(&self, demand: &Demand) -> (u64, Option<u64>) {
if let Some(identity) = &demand.per_test {
let path = test_history_path(&self.dir.join(TEST_HISTORY_DIR), &identity.binary);
let history = lookup_test_history(&path, &identity.binary, &identity.test);
if history.header.count > 0 {
refresh_test_history(&path);
}
identity.known.set(Some(history.test.unwrap_or_default()));
let measurement = history
.test
.or_else(|| (history.header.count > 0).then_some(history.header.maximum));
let cores = measurement
.and_then(|measurement| measurement.cores)
.unwrap_or(1)
.max(1);
if self.bytes_per_permit == 0 {
return (cores.min(self.capacity), None);
}
let predicted = measurement
.and_then(|measurement| measurement.peak)
.unwrap_or_else(|| UNMEASURED_TEST_WEIGHT.saturating_mul(self.bytes_per_permit));
let weight = predicted
.div_ceil(self.bytes_per_permit)
.max(cores)
.clamp(1, self.capacity);
return (weight, Some(predicted));
}
let learned = demand
.learns_cpu
.then(|| {
read_ledger(&self.ledger_path())
.cpus
.get(&demand.name)
.copied()
})
.flatten();
let threads = learned
.or(demand.threads)
.unwrap_or_else(|| self.capacity.div_ceil(2))
.max(1);
let link_floor = demand.links.then_some(LINK_WEIGHT);
if self.bytes_per_permit == 0 {
return (
link_floor.unwrap_or(1).max(threads).min(self.capacity),
None,
);
}
let ledger = read_ledger(&self.ledger_path());
let recorded = ledger
.crates
.get(&ledger_key(&demand.name, demand.links))
.copied();
let link_bytes = link_floor.map(|weight| {
let window = match &demand.profile {
Some(profile) => ledger
.profile_link_peaks
.get(profile)
.map_or(&[][..], Vec::as_slice),
None => ledger.link_peaks.as_slice(),
};
let measured = (window.len() >= MIN_LINK_SAMPLES)
.then(|| window.iter().copied().max())
.flatten();
measured.unwrap_or_else(|| weight.saturating_mul(self.bytes_per_permit))
});
let predicted = recorded.or(link_bytes);
let weight = predicted
.map_or(1, |bytes| bytes.div_ceil(self.bytes_per_permit))
.max(threads)
.clamp(1, self.capacity);
(weight, predicted)
}
fn try_admit(&self, weight: u64, predicted: Option<u64>) -> Result<Option<Permit>> {
let leases = self.dir.join(LEASES_DIR);
std::fs::create_dir_all(&leases)
.wrap_err_with(|| format!("failed to create {}", leases.display()))?;
let registrar_path = self.dir.join(POOL_LOCK);
let mut registrar = fslock::LockFile::open(®istrar_path)?;
registrar.lock()?;
let live = scan_leases(&leases)?;
let now = (self.clock)();
let pressure_state = (self.pressure && self.bytes_per_permit > 0)
.then(|| crate::pressure::domain_state(&self.dir, self.pressure_domain));
let mut pressure = pressure_state
.as_ref()
.and_then(|path| crate::pressure::sample(path, now, self.pressure_probe).ok());
if self.pressure
&& self.bytes_per_permit > 0
&& !live.is_empty()
&& crate::supervision::resumes_pending(&self.dir, now)
{
return Ok(None);
}
if !live.is_empty()
&& let Some(state) = &pressure
&& state.valid
&& (state.pressured
|| (state.recovered_ms.is_some()
&& state.last_admission_ms.is_some_and(|last| {
now.saturating_sub(last) < crate::pressure::INTERVAL_MS
})))
{
return Ok(None);
}
let capacity = self.capacity - self.reserved();
let used: u64 = live.iter().map(|lease| lease.weight).sum();
if let Some((build_id, build_capacity)) = &self.build {
let build_used = live
.iter()
.filter(|lease| lease.build_id.as_ref() == Some(build_id))
.map(|lease| lease.weight)
.sum::<u64>();
if build_used > 0 && build_used.saturating_add(weight) > *build_capacity {
return Ok(None);
}
}
if used > 0 && used.saturating_add(weight) > capacity {
return Ok(None);
}
if used > 0
&& let Some(needed) = predicted
&& let Some(available) = (self.available_memory)()
&& available < needed
{
return Ok(None);
}
let permit = self.grant(&leases, weight)?;
if let Some((path, state)) = pressure_state.as_ref().zip(pressure.as_mut()) {
state.last_admission_ms = Some(now);
let _ = crate::pressure::save(path, state);
}
Ok(Some(permit))
}
fn grant(&self, leases: &Path, weight: u64) -> Result<Permit> {
let lease = serde_json::to_vec(&Lease {
version: LEASE_VERSION,
weight,
priority: self.priority.as_str().into(),
build_id: self.build.as_ref().map(|(id, _)| id.clone()),
})?;
let mut last = None;
for _ in 0..LEASE_NAME_ATTEMPTS {
let path = leases.join(format!(
"{}-{}-{}",
std::process::id(),
process_token(),
(self.lease_nonce)()
));
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(mut file) => {
std::io::Write::write_all(&mut file, &lease)
.wrap_err_with(|| format!("failed to write {}", path.display()))?;
drop(file);
let mut lock = fslock::LockFile::open(&path)?;
if !lock.try_lock()? {
eyre::bail!("a freshly created lease was already locked");
}
return Ok(Permit {
lock: Some(lock),
path,
});
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
last = Some(error);
}
Err(error) => {
return Err(error)
.wrap_err_with(|| format!("failed to create {}", path.display()));
}
}
}
Err(last.expect("a name is only retried after a collision"))
.wrap_err("failed to name a lease that was not already taken")
}
fn reserved(&self) -> u64 {
if self.priority == SchedulerPriority::Low && self.priority_wait_is_fresh() {
(self.capacity / 4).max(1).min(self.capacity - 1)
} else {
0
}
}
fn priority_wait_is_fresh(&self) -> bool {
std::fs::metadata(self.dir.join(PRIORITY_WAIT_STAMP))
.and_then(|metadata| metadata.modified())
.ok()
.and_then(|modified| modified.elapsed().ok())
.is_some_and(|since| since < PRIORITY_WAIT_FRESHNESS)
}
fn note_compilation(&self, demand: &Demand, status: &ExitStatus) {
if self.bytes_per_permit == 0 {
return;
}
let Some(measured) = child_peak_rss_bytes() else {
return;
};
let Some(peak) = ledger_peak(
measured,
oom_killed(status),
demand.links,
status.success(),
self.bytes_per_permit,
) else {
return;
};
if let Err(error) = self.record_link_peak(
&ledger_key(&demand.name, demand.links),
peak,
demand.links,
demand.profile.as_deref(),
) {
debug!("compiler memory was not recorded: {error:#}");
}
}
fn ledger_path(&self) -> PathBuf {
self.dir.join(LEDGER_FILE)
}
fn record_test_measurement(
&self,
identity: &TestIdentity,
peak: Option<u64>,
cores: Option<u64>,
) -> Result<()> {
let raises = |value: Option<u64>, known: Option<u64>| {
value.is_some_and(|value| known.is_none_or(|known| value > known))
};
if let Some(known) = identity.known.get()
&& !raises(peak, known.peak)
&& !raises(cores, known.cores)
{
return Ok(());
}
self.record_test_history(&identity.binary, &identity.test, peak, cores)
}
fn record_test_history(
&self,
binary: &str,
test: &str,
peak: Option<u64>,
cores: Option<u64>,
) -> Result<()> {
let dir = self.dir.join(TEST_HISTORY_DIR);
std::fs::create_dir_all(&dir)
.wrap_err_with(|| format!("failed to create {}", dir.display()))?;
let path = test_history_path(&dir, binary);
let mut lock = fslock::LockFile::open(&self.dir.join(TEST_HISTORY_LOCK))?;
lock.lock()?;
let hash = test_history_hash(test);
let mut records = read_test_history_records(&path, binary);
let position = records.binary_search_by_key(&hash, |record| record.hash);
let current = position
.as_ref()
.ok()
.map(|index| records[*index].measurement)
.unwrap_or_default();
let updated = raise_measurement(
current,
TestMeasurement {
peak: peak.filter(|value| *value > 0),
cores: cores.filter(|value| *value > 0),
},
);
if updated == current {
return Ok(());
}
match position {
Ok(index) => records[index].measurement = updated,
Err(index) => records.insert(
index,
TestHistoryRecord {
hash,
measurement: updated,
},
),
}
enforce_test_history_cap(&mut records, MAX_TEST_HISTORY_ENTRIES);
let contents = encode_test_history(binary, &records);
crate::util::write_advisory(&path, &contents)
.wrap_err_with(|| format!("failed to write {}", path.display()))?;
Ok(())
}
#[cfg(test)]
fn record_peak(&self, name: &str, peak: u64, links: bool) -> Result<()> {
self.record_link_peak(name, peak, links, None)
}
fn record_link_peak(
&self,
name: &str,
peak: u64,
links: bool,
profile: Option<&str>,
) -> Result<()> {
std::fs::create_dir_all(&self.dir)
.wrap_err_with(|| format!("failed to create {}", self.dir.display()))?;
let mut lock = fslock::LockFile::open(&self.dir.join(LEDGER_LOCK))?;
lock.lock()?;
let path = self.ledger_path();
let mut ledger = read_ledger(&path);
ledger.version = LEDGER_VERSION;
if links {
let window = match profile {
Some(profile) => ledger
.profile_link_peaks
.entry(profile.to_string())
.or_default(),
None => &mut ledger.link_peaks,
};
window.push(peak);
let surplus = window.len().saturating_sub(MAX_LINK_SAMPLES);
window.drain(..surplus);
while ledger.profile_link_peaks.len() > MAX_LEDGER_ENTRIES {
ledger.profile_link_peaks.pop_first();
}
}
let known = ledger.crates.get(name).is_some_and(|known| *known >= peak);
if known && !links {
return Ok(());
}
if !known {
ledger.crates.insert(name.to_string(), peak);
}
while ledger.crates.len() > MAX_LEDGER_ENTRIES {
let smallest = ledger
.crates
.iter()
.min_by_key(|(_, peak)| **peak)
.map(|(name, _)| name.clone())
.expect("an overfull ledger has entries");
ledger.crates.remove(&smallest);
}
let mut contents = serde_json::to_vec(&ledger)?;
contents.push(b'\n');
crate::util::write_advisory(&path, &contents)
}
}
impl Pool {
fn record_cpu(&self, name: &str, cores: u64) -> Result<()> {
std::fs::create_dir_all(&self.dir)
.wrap_err_with(|| format!("failed to create {}", self.dir.display()))?;
let mut lock = fslock::LockFile::open(&self.dir.join(LEDGER_LOCK))?;
lock.lock()?;
let path = self.ledger_path();
let mut ledger = read_ledger(&path);
if ledger.cpus.get(name).is_some_and(|known| *known >= cores) {
return Ok(());
}
ledger.version = LEDGER_VERSION;
ledger.cpus.insert(name.to_string(), cores);
while ledger.cpus.len() > MAX_LEDGER_ENTRIES {
let smallest = ledger
.cpus
.iter()
.min_by_key(|(_, cores)| **cores)
.map(|(name, _)| name.clone())
.expect("an overfull ledger has entries");
ledger.cpus.remove(&smallest);
}
let mut contents = serde_json::to_vec(&ledger)?;
contents.push(b'\n');
crate::util::write_advisory(&path, &contents)
}
}
fn ledger_key(name: &str, links: bool) -> String {
if links {
format!("{name} [link]")
} else {
name.to_string()
}
}
fn link_profile(arguments: &[std::ffi::OsString]) -> String {
let (mut opt, mut lto, mut units, mut debug) = (None, None, None, None);
let mut arguments = arguments.iter().filter_map(|argument| argument.to_str());
while let Some(argument) = arguments.next() {
let setting = match argument {
"-C" | "--codegen" => arguments.next(),
"-O" => {
opt = Some("3");
continue;
}
"-g" => {
debug = Some("2");
continue;
}
_ => argument
.strip_prefix("--codegen=")
.or_else(|| argument.strip_prefix("-C")),
};
let Some(setting) = setting else { continue };
let (key, value) = setting.split_once('=').unwrap_or((setting, ""));
match key {
"opt-level" => opt = Some(value),
"lto" => {
lto = Some(match value {
"" | "yes" | "y" | "on" | "true" | "fat" => "fat",
"no" | "n" | "off" | "false" => "off",
other => other,
})
}
"codegen-units" => units = Some(value),
"debuginfo" => debug = Some(value),
_ => {}
}
}
format!(
"opt{} lto-{} cgu{} debug{}",
opt.unwrap_or("0"),
lto.unwrap_or("default"),
units.unwrap_or("-"),
debug.unwrap_or("0"),
)
}
fn read_ledger(path: &Path) -> MemoryLedger {
std::fs::read(path)
.ok()
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
.unwrap_or_default()
}
fn test_history_path(dir: &Path, binary: &str) -> PathBuf {
use sha2::Digest as _;
let digest = sha2::Sha256::digest(binary.as_bytes());
dir.join(format!("{}.hist", hex::encode(&digest[..8])))
}
fn test_history_hash(test: &str) -> u64 {
use sha2::Digest as _;
let digest = sha2::Sha256::digest(test.as_bytes());
u64::from_le_bytes(digest[..8].try_into().expect("SHA-256 has eight bytes"))
}
fn test_history_binary_tag(binary: &str) -> u64 {
use sha2::Digest as _;
let digest = sha2::Sha256::digest(binary.as_bytes());
u64::from_le_bytes(digest[8..16].try_into().expect("SHA-256 has sixteen bytes"))
}
fn refresh_test_history(path: &Path) {
let old = std::fs::metadata(path)
.and_then(|metadata| metadata.modified())
.ok()
.and_then(|modified| modified.elapsed().ok())
.is_some_and(|age| age > TEST_HISTORY_REFRESH);
if old && let Ok(file) = std::fs::OpenOptions::new().append(true).open(path) {
let _ = file.set_modified(std::time::SystemTime::now());
}
}
fn parse_test_history_header(
bytes: &[u8],
file_len: u64,
binary: &str,
) -> Option<TestHistoryHeader> {
if bytes.len() != TEST_HISTORY_HEADER_LEN as usize
|| file_len < TEST_HISTORY_HEADER_LEN
|| &bytes[..8] != TEST_HISTORY_MAGIC
|| read_u64(&bytes[8..16]) != test_history_binary_tag(binary)
{
return None;
}
Some(TestHistoryHeader {
count: (file_len - TEST_HISTORY_HEADER_LEN) / TEST_HISTORY_RECORD_LEN,
maximum: TestMeasurement {
peak: nonzero(read_u64(&bytes[16..24])),
cores: nonzero(read_u64(&bytes[24..32])),
},
})
}
fn read_test_history_header(file: &File, binary: &str) -> Option<TestHistoryHeader> {
let file_len = file.metadata().ok()?.len();
if file_len < TEST_HISTORY_HEADER_LEN {
return None;
}
let mut bytes = [0; TEST_HISTORY_HEADER_LEN as usize];
read_exact_at(file, &mut bytes, 0).ok()?;
parse_test_history_header(&bytes, file_len, binary)
}
fn lookup_test_history(path: &Path, binary: &str, test: &str) -> TestHistoryLookup {
let Ok(file) = File::open(path) else {
return TestHistoryLookup::default();
};
let Some(header) = read_test_history_header(&file, binary) else {
return TestHistoryLookup::default();
};
let hash = test_history_hash(test);
let mut low = 0;
let mut high = header.count;
let mut found = None;
while low < high {
let middle = low + (high - low) / 2;
let offset = TEST_HISTORY_HEADER_LEN + middle * TEST_HISTORY_RECORD_LEN;
let mut bytes = [0; TEST_HISTORY_RECORD_LEN as usize];
if read_exact_at(&file, &mut bytes, offset).is_err() {
break;
}
let record = decode_test_history_record(&bytes);
match record.hash.cmp(&hash) {
std::cmp::Ordering::Less => low = middle + 1,
std::cmp::Ordering::Greater => high = middle,
std::cmp::Ordering::Equal => {
found = Some(record.measurement);
break;
}
}
}
TestHistoryLookup {
test: found,
header,
}
}
fn read_test_history_records(path: &Path, binary: &str) -> Vec<TestHistoryRecord> {
let Ok(contents) = std::fs::read(path) else {
return Vec::new();
};
let Some(header) = contents
.get(..TEST_HISTORY_HEADER_LEN as usize)
.and_then(|bytes| parse_test_history_header(bytes, contents.len() as u64, binary))
else {
return Vec::new();
};
let mut records: Vec<_> = contents[TEST_HISTORY_HEADER_LEN as usize..]
.chunks_exact(TEST_HISTORY_RECORD_LEN as usize)
.take(header.count as usize)
.map(decode_test_history_record)
.collect();
records.sort_unstable_by_key(|record| record.hash);
let mut unique: Vec<TestHistoryRecord> = Vec::with_capacity(records.len());
for record in records {
if let Some(previous) = unique
.last_mut()
.filter(|previous| previous.hash == record.hash)
{
previous.measurement = raise_measurement(previous.measurement, record.measurement);
} else {
unique.push(record);
}
}
unique
}
fn decode_test_history_record(bytes: &[u8]) -> TestHistoryRecord {
TestHistoryRecord {
hash: read_u64(&bytes[..8]),
measurement: TestMeasurement {
peak: nonzero(read_u64(&bytes[8..16])),
cores: nonzero(read_u64(&bytes[16..24])),
},
}
}
fn encode_test_history(binary: &str, records: &[TestHistoryRecord]) -> Vec<u8> {
let maximum = records
.iter()
.fold(TestMeasurement::default(), |maximum, record| {
raise_measurement(maximum, record.measurement)
});
let mut bytes = Vec::with_capacity(
TEST_HISTORY_HEADER_LEN as usize + records.len() * TEST_HISTORY_RECORD_LEN as usize,
);
bytes.extend_from_slice(TEST_HISTORY_MAGIC);
bytes.extend_from_slice(&test_history_binary_tag(binary).to_le_bytes());
bytes.extend_from_slice(&maximum.peak.unwrap_or(0).to_le_bytes());
bytes.extend_from_slice(&maximum.cores.unwrap_or(0).to_le_bytes());
for record in records {
bytes.extend_from_slice(&record.hash.to_le_bytes());
bytes.extend_from_slice(&record.measurement.peak.unwrap_or(0).to_le_bytes());
bytes.extend_from_slice(&record.measurement.cores.unwrap_or(0).to_le_bytes());
}
bytes
}
fn read_u64(bytes: &[u8]) -> u64 {
u64::from_le_bytes(bytes.try_into().expect("history field is eight bytes"))
}
fn nonzero(value: u64) -> Option<u64> {
(value != 0).then_some(value)
}
#[cfg(unix)]
fn read_at(file: &File, buffer: &mut [u8], offset: u64) -> std::io::Result<usize> {
use std::os::unix::fs::FileExt as _;
file.read_at(buffer, offset)
}
#[cfg(windows)]
fn read_at(file: &File, buffer: &mut [u8], offset: u64) -> std::io::Result<usize> {
use std::os::windows::fs::FileExt as _;
file.seek_read(buffer, offset)
}
#[cfg(not(any(unix, windows)))]
fn read_at(file: &File, buffer: &mut [u8], offset: u64) -> std::io::Result<usize> {
use std::io::{Read as _, Seek as _, SeekFrom};
let mut file = file.try_clone()?;
file.seek(SeekFrom::Start(offset))?;
file.read(buffer)
}
fn read_exact_at(file: &File, mut buffer: &mut [u8], mut offset: u64) -> std::io::Result<()> {
while !buffer.is_empty() {
match read_at(file, buffer, offset) {
Ok(0) => return Err(std::io::ErrorKind::UnexpectedEof.into()),
Ok(read) => {
offset = offset
.checked_add(read as u64)
.ok_or(std::io::ErrorKind::InvalidInput)?;
buffer = &mut buffer[read..];
}
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
Err(error) => return Err(error),
}
}
Ok(())
}
fn enforce_test_history_cap(records: &mut Vec<TestHistoryRecord>, limit: usize) -> bool {
let mut capped = false;
while records.len() > limit {
let smallest = records
.iter()
.enumerate()
.min_by(|(_, left), (_, right)| {
left.measurement
.peak
.unwrap_or(0)
.cmp(&right.measurement.peak.unwrap_or(0))
.then_with(|| {
left.measurement
.cores
.unwrap_or(0)
.cmp(&right.measurement.cores.unwrap_or(0))
})
.then_with(|| left.hash.cmp(&right.hash))
})
.map(|(index, _)| index)
.expect("an overfull test history has entries");
records.remove(smallest);
capped = true;
}
capped
}
fn raise_measurement(current: TestMeasurement, incoming: TestMeasurement) -> TestMeasurement {
TestMeasurement {
peak: match (current.peak, incoming.peak) {
(Some(current), Some(incoming)) => Some(current.max(incoming)),
(current, incoming) => current.or(incoming),
},
cores: match (current.cores, incoming.cores) {
(Some(current), Some(incoming)) => Some(current.max(incoming)),
(current, incoming) => current.or(incoming),
},
}
}
fn scan_leases(leases: &Path) -> Result<Vec<Lease>> {
let mut live = Vec::new();
for entry in std::fs::read_dir(leases)? {
let path = entry?.path();
let mut lock = fslock::LockFile::open(&path)?;
if lock.try_lock()? {
drop(lock);
let _ = std::fs::remove_file(&path);
continue;
}
let lease = std::fs::read(&path)
.ok()
.and_then(|bytes| serde_json::from_slice::<Lease>(&bytes).ok())
.map_or_else(
|| Lease {
version: LEASE_VERSION,
weight: 1,
priority: SchedulerPriority::Normal.as_str().into(),
build_id: None,
},
|mut lease| {
lease.weight = lease.weight.max(1);
lease
},
);
live.push(lease);
}
Ok(live)
}
fn ledger_peak(
measured: u64,
oom_killed: bool,
links: bool,
succeeded: bool,
bytes_per_permit: u64,
) -> Option<u64> {
if oom_killed {
return Some(
measured
.saturating_mul(2)
.max(bytes_per_permit.saturating_add(1)),
);
}
if links && succeeded {
return Some(measured);
}
(measured > bytes_per_permit).then_some(measured)
}
#[cfg(unix)]
fn child_peak_rss_bytes() -> Option<u64> {
let usage = children_usage()?;
let maxrss = u64::try_from(usage.ru_maxrss).ok()?;
let bytes = if cfg!(target_vendor = "apple") {
maxrss
} else {
maxrss.saturating_mul(1024)
};
Some(bytes).filter(|bytes| *bytes > 0)
}
#[cfg(not(unix))]
fn child_peak_rss_bytes() -> Option<u64> {
None
}
#[cfg(unix)]
pub(crate) fn child_cpu_time() -> Option<Duration> {
let usage = children_usage()?;
let time = |value: libc::timeval| {
Some(Duration::new(
u64::try_from(value.tv_sec).ok()?,
u32::try_from(value.tv_usec).ok()?.saturating_mul(1000),
))
};
Some(time(usage.ru_utime)? + time(usage.ru_stime)?)
}
#[cfg(not(unix))]
pub(crate) fn child_cpu_time() -> Option<Duration> {
None
}
#[cfg(unix)]
fn children_usage() -> Option<libc::rusage> {
unsafe {
let mut usage = std::mem::zeroed::<libc::rusage>();
(libc::getrusage(libc::RUSAGE_CHILDREN, &mut usage) == 0).then_some(usage)
}
}
#[cfg(unix)]
fn oom_killed(status: &ExitStatus) -> bool {
use std::os::unix::process::ExitStatusExt as _;
status.signal() == Some(libc::SIGKILL)
}
#[cfg(not(unix))]
fn oom_killed(_status: &ExitStatus) -> bool {
false
}
fn jittered(delay: Duration) -> Duration {
use rand::RngExt as _;
delay + delay.mul_f64(rand::rng().random_range(0.0..0.5))
}
#[cfg(test)]
#[path = "scheduler_tests.rs"]
mod tests;