use std::collections::HashSet;
use std::fs::{File, OpenOptions, TryLockError};
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
use std::time::{Duration, Instant};
use cubecl::hash::StableHasher;
use crate::cache::compilation_cache_dir;
use crate::frame::{FrameLayout, PlaneOptions};
const WAIT_LIMIT: Duration = Duration::from_secs(180);
const POLL_INTERVAL: Duration = Duration::from_millis(100);
static CLAIMED_KEYS: LazyLock<Mutex<HashSet<u128>>> = LazyLock::new(|| Mutex::new(HashSet::new()));
fn claim_key(key: u128) -> bool {
CLAIMED_KEYS
.lock()
.expect("warm-up key mutex poisoned")
.insert(key)
}
fn release_key(key: u128) {
CLAIMED_KEYS
.lock()
.expect("warm-up key mutex poisoned")
.remove(&key);
}
pub fn kernel_key(options: &PlaneOptions, layout: FrameLayout) -> u128 {
StableHasher::hash_one(&format!("{}|{options:?}|{layout:?}", env!("CARGO_PKG_VERSION")))
}
#[derive(Debug)]
pub struct WarmUp {
lock: File,
stamp: PathBuf,
key: u128,
}
impl WarmUp {
pub fn begin(key: u128) -> Option<Self> {
Self::begin_in(compilation_cache_dir()?, key, WAIT_LIMIT)
}
fn begin_in(dir: &Path, key: u128, wait_limit: Duration) -> Option<Self> {
if !claim_key(key) {
return None;
}
let held = Self::acquire(dir, key, wait_limit);
if held.is_none() {
release_key(key);
}
held
}
fn acquire(dir: &Path, key: u128, wait_limit: Duration) -> Option<Self> {
let stamp = dir.join(format!("warm-{key:032x}.stamp"));
if stamp.exists() {
return None;
}
let lock = open_lock_file(&dir.join(format!("warm-{key:032x}.lock")))?;
if !wait_for_lock(&lock, wait_limit) {
return None;
}
if stamp.exists() {
let _ = lock.unlock();
return None;
}
tracing::debug!(?stamp, "compiling kernels for a cold cache");
Some(Self { lock, stamp, key })
}
pub fn finish(self) {
if let Err(err) = std::fs::write(&self.stamp, b"") {
tracing::debug!(stamp = ?self.stamp, %err, "cannot write the kernel warm-up stamp");
}
}
}
impl Drop for WarmUp {
fn drop(&mut self) {
let _ = self.lock.unlock();
release_key(self.key);
}
}
fn open_lock_file(path: &Path) -> Option<File> {
match OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(path)
{
Ok(file) => Some(file),
Err(err) => {
tracing::debug!(?path, %err, "cannot open the kernel warm-up lock, compiling unqueued");
None
},
}
}
fn wait_for_lock(lock: &File, wait_limit: Duration) -> bool {
let start = Instant::now();
loop {
match lock.try_lock() {
Ok(()) => return true,
Err(TryLockError::WouldBlock) => {},
Err(TryLockError::Error(err)) => {
tracing::debug!(%err, "cannot take the kernel warm-up lock, compiling unqueued");
return false;
},
}
if start.elapsed() >= wait_limit {
tracing::warn!(
"waited {:?} for another process to compile kernels, compiling for ourselves",
wait_limit,
);
return false;
}
std::thread::sleep(POLL_INTERVAL);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(n: u128) -> u128 {
0xa5a5_0000_0000_0000_0000_0000_0000_0000 + n
}
const BRIEFLY: Duration = Duration::from_millis(50);
#[test]
fn a_lock_held_elsewhere_keeps_this_process_out() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(format!("warm-{:032x}.lock", key(1)));
let elsewhere = open_lock_file(&path).unwrap();
elsewhere.lock().unwrap();
assert!(
WarmUp::begin_in(dir.path(), key(1), BRIEFLY).is_none(),
"a caller gives up rather than compiling alongside the process ahead",
);
}
#[test]
fn one_process_takes_one_place_per_key() {
let dir = tempfile::tempdir().unwrap();
let first = WarmUp::begin_in(dir.path(), key(6), BRIEFLY);
assert!(first.is_some(), "the first caller compiles");
assert!(
WarmUp::begin_in(dir.path(), key(6), BRIEFLY).is_none(),
"the second caller carries on rather than waiting for itself",
);
}
#[test]
fn a_released_place_can_be_taken_again() {
let dir = tempfile::tempdir().unwrap();
drop(WarmUp::begin_in(dir.path(), key(7), BRIEFLY));
assert!(
WarmUp::begin_in(dir.path(), key(7), BRIEFLY).is_some(),
"the key is free again once the place is given up",
);
}
#[test]
fn a_finished_warm_up_lets_the_next_process_straight_through() {
let dir = tempfile::tempdir().unwrap();
WarmUp::begin_in(dir.path(), key(2), BRIEFLY).unwrap().finish();
assert!(
WarmUp::begin_in(dir.path(), key(2), BRIEFLY).is_none(),
"a warm cache needs no queue",
);
}
#[test]
fn an_abandoned_warm_up_leaves_the_cache_cold() {
let dir = tempfile::tempdir().unwrap();
drop(WarmUp::begin_in(dir.path(), key(3), BRIEFLY));
assert!(
WarmUp::begin_in(dir.path(), key(3), BRIEFLY).is_some(),
"no stamp means the kernels still need compiling",
);
}
fn options() -> PlaneOptions {
PlaneOptions {
accelerators: Vec::new(),
device: crate::Device::Default,
intent: crate::ChannelIntent::LumaChroma,
mode: crate::DenoisingMode::Temporal { radius: 2 },
algorithm: crate::Algorithm::default(),
luma_strength: None,
chroma_strength: None,
luma_lambda_ht: None,
chroma_lambda_ht: None,
luma_mismatch_scale: None,
chroma_mismatch_scale: None,
}
}
fn layout() -> FrameLayout {
FrameLayout {
width: 1920,
height: 1080,
subsampling: crate::Subsampling::Yuv420,
depth: crate::Depth::Eight,
}
}
#[test]
fn the_same_settings_give_the_same_key() {
assert_eq!(kernel_key(&options(), layout()), kernel_key(&options(), layout()));
}
#[test]
fn a_different_depth_gives_a_different_key() {
let ten_bit = FrameLayout {
depth: crate::Depth::Ten,
..layout()
};
assert_ne!(kernel_key(&options(), layout()), kernel_key(&options(), ten_bit));
}
#[test]
fn a_different_radius_gives_a_different_key() {
let wider = PlaneOptions {
mode: crate::DenoisingMode::Temporal { radius: 3 },
..options()
};
assert_ne!(kernel_key(&options(), layout()), kernel_key(&wider, layout()));
}
#[test]
fn different_kernels_do_not_wait_for_each_other() {
let dir = tempfile::tempdir().unwrap();
let first = WarmUp::begin_in(dir.path(), key(4), BRIEFLY);
let second = WarmUp::begin_in(dir.path(), key(5), BRIEFLY);
assert!(
first.is_some() && second.is_some(),
"separate keys queue separately"
);
}
}