use std::{
future::Future, panic::{AssertUnwindSafe, catch_unwind}, pin::Pin, sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError}, task::{Context, Poll, Waker}
};
use bigdecimal::BigDecimal;
use chrono::{DateTime, Utc};
use crate::{Backend, Error, Interpolation, Interpolator, Point, Result};
impl Interpolator {
#[must_use = "dropping the future cancels the interpolation if it hasn't started"]
pub fn spawn(self, points: Vec<Point>, start: DateTime<Utc>, end: DateTime<Utc>) -> InterpolationFuture<BigDecimal> {
let (auto_gpu, gpu) = self.for_spawn();
InterpolationFuture::spawn(gpu, move || self.run_points(&points, start, end, auto_gpu))
}
#[must_use = "dropping the future cancels the interpolation if it hasn't started"]
pub fn spawn_f64(self, timestamps: Vec<DateTime<Utc>>, values: Vec<f64>, start: DateTime<Utc>, end: DateTime<Utc>) -> InterpolationFuture<f64> {
let (auto_gpu, gpu) = self.for_spawn();
InterpolationFuture::spawn(gpu, move || self.run_columns(×tamps, &values, start, end, auto_gpu))
}
fn for_spawn(&self) -> (bool, bool) {
match self.backend {
Backend::Gpu => {
let _ = crate::prewarm_gpu();
(true, true)
}
Backend::Auto => {
let started = crate::gpu::started();
(started, started)
}
_ => (true, false),
}
}
}
#[derive(Debug)]
pub struct InterpolationFuture<V> {
shared: Arc<Shared<V>>,
gpu: bool,
}
#[derive(Debug)]
struct Shared<V> {
state: Mutex<State<V>>,
finished: Condvar,
}
#[derive(Debug)]
enum State<V> {
Queued(Option<Waker>),
Running(Option<Waker>),
Done(Result<Interpolation<V>>),
Taken,
Cancelled,
}
impl<V> Shared<V> {
const fn new(state: State<V>) -> Self {
Self { state: Mutex::new(state), finished: Condvar::new() }
}
fn lock(&self) -> MutexGuard<'_, State<V>> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
fn start(&self) -> bool {
let mut state = self.lock();
let State::Queued(waker) = &mut *state else { return false };
let waker = waker.take();
*state = State::Running(waker);
true
}
}
impl<V: Send + 'static> InterpolationFuture<V> {
fn spawn(gpu: bool, work: impl FnOnce() -> Result<Interpolation<V>> + Send + 'static) -> Self {
if rayon::current_thread_index().is_some() {
return Self { shared: Arc::new(Shared::new(State::Done(caught(work)))), gpu };
}
let shared = Arc::new(Shared::new(State::Queued(None)));
let task = Arc::clone(&shared);
rayon::spawn(move || {
if !task.start() {
return;
}
let result = caught(work);
let previous = std::mem::replace(&mut *task.lock(), State::Done(result));
task.finished.notify_all();
if let State::Running(Some(waker)) = previous {
waker.wake();
}
});
Self { shared, gpu }
}
}
impl<V> Drop for InterpolationFuture<V> {
fn drop(&mut self) {
let running = {
let mut state = self.shared.lock();
if matches!(*state, State::Queued(_)) {
*state = State::Cancelled;
false
} else {
matches!(*state, State::Running(_))
}
};
if running && self.gpu && rayon::current_thread_index().is_none() {
drop(self.shared.finished.wait_while(self.shared.lock(), |state| matches!(state, State::Running(_))).unwrap_or_else(PoisonError::into_inner));
}
}
}
fn caught<V>(work: impl FnOnce() -> Result<Interpolation<V>>) -> Result<Interpolation<V>> {
catch_unwind(AssertUnwindSafe(work)).unwrap_or_else(|panic| {
let message = panic.downcast_ref::<&str>().map(|s| (*s).to_owned()).or_else(|| panic.downcast_ref::<String>().cloned()).unwrap_or_default();
Err(Error::Task(format!("the interpolation panicked: {message}")))
})
}
impl<V> Future for InterpolationFuture<V> {
type Output = Result<Interpolation<V>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut state = self.shared.lock();
match &mut *state {
State::Queued(waker) | State::Running(waker) => {
match waker {
Some(w) if w.will_wake(cx.waker()) => {}
_ => *waker = Some(cx.waker().clone()),
}
Poll::Pending
}
State::Done(_) => match std::mem::replace(&mut *state, State::Taken) {
State::Done(result) => Poll::Ready(result),
_ => Poll::Ready(Err(Error::Task("the interpolation's result was lost".to_owned()))),
},
State::Taken => Poll::Ready(Err(Error::Task("polled after the interpolation's result was taken".to_owned()))),
State::Cancelled => Poll::Ready(Err(Error::Task("the interpolation was cancelled".to_owned()))),
}
}
}
#[cfg(test)]
mod tests {
use std::{
sync::{
Barrier, atomic::{AtomicBool, AtomicUsize, Ordering}, mpsc
}, task::Wake, thread::{self, Thread}, time::{Duration, Instant}
};
use chrono::TimeZone;
use super::*;
use crate::{Resolution, Spline};
struct Unpark(Thread, AtomicUsize);
impl Wake for Unpark {
fn wake(self: Arc<Self>) {
self.1.fetch_add(1, Ordering::SeqCst);
self.0.unpark();
}
}
fn block_on<F: Future + Unpin>(mut f: F) -> (F::Output, usize) {
let unpark = Arc::new(Unpark(thread::current(), AtomicUsize::new(0)));
let waker = Waker::from(Arc::clone(&unpark));
let mut cx = Context::from_waker(&waker);
loop {
if let Poll::Ready(v) = Pin::new(&mut f).poll(&mut cx) {
return (v, unpark.1.load(Ordering::SeqCst));
}
thread::park();
}
}
fn at(secs: i64) -> DateTime<Utc> {
Utc.timestamp_opt(secs, 0).single().expect("valid")
}
#[test]
fn resolves_to_what_run_returns() {
let interpolator = Interpolator::new(Spline::Cubic, Resolution::Milliseconds);
let ts: Vec<_> = (0..50).map(|i| at(i * 3)).collect();
let vs: Vec<f64> = (0..50).map(|i| f64::from(i).sin()).collect();
let (spawned, _) = block_on(interpolator.spawn_f64(ts.clone(), vs.clone(), at(-5), at(160)));
assert_eq!(spawned, interpolator.run_f64(&ts, &vs, at(-5), at(160)));
let points: Vec<Point> = ts.iter().zip(&vs).map(|(&t, &v)| Point::new(t, BigDecimal::try_from(v).expect("finite"))).collect();
let (spawned, _) = block_on(interpolator.spawn(points.clone(), at(0), at(10)));
assert_eq!(spawned, interpolator.run(&points, at(0), at(10)));
let (err, _) = block_on(interpolator.spawn(Vec::new(), at(0), at(1)));
assert_eq!(err, Err(Error::NoPoints));
}
#[test]
fn wakes_the_task_that_is_waiting() {
let interpolator = Interpolator::new(Spline::Linear, Resolution::Microseconds).backend(crate::Backend::Cpu);
let (out, wakes) = block_on(interpolator.spawn_f64(vec![at(0), at(100)], vec![0.0, 100.0], at(0), at(10)));
assert_eq!(out.expect("runs").len(), 10_000_001);
assert_eq!(wakes, 1);
}
#[test]
fn a_second_poll_after_completion_is_an_error() {
let mut f = Interpolator::new(Spline::Linear, Resolution::Seconds).spawn_f64(vec![at(0), at(1)], vec![0.0, 1.0], at(0), at(1));
let (first, _) = block_on(&mut f);
assert!(first.is_ok());
let (again, _) = block_on(&mut f);
assert!(matches!(again, Err(Error::Task(_))), "{again:?}");
}
#[test]
fn a_panicking_task_becomes_an_error() {
let f: InterpolationFuture<f64> = InterpolationFuture::spawn(false, || panic!("boom"));
let (out, _) = block_on(f);
assert_eq!(out, Err(Error::Task("the interpolation panicked: boom".to_owned())));
}
const PATIENCE: Duration = Duration::from_secs(30);
fn wait_until_freed<T>(shared: &std::sync::Weak<T>) {
let deadline = Instant::now() + PATIENCE;
while shared.upgrade().is_some() {
assert!(Instant::now() < deadline, "the task's state was never freed");
thread::yield_now();
}
}
#[test]
fn dropping_a_started_future_lets_the_work_finish() {
let (started, has_started) = mpsc::channel();
let (finished, has_finished) = mpsc::channel();
let f: InterpolationFuture<f64> = InterpolationFuture::spawn(false, move || {
started.send(()).expect("the test is waiting");
thread::sleep(Duration::from_millis(50));
finished.send(()).expect("the test is waiting");
Err(Error::NoPoints)
});
has_started.recv_timeout(PATIENCE).expect("the work started");
let shared = Arc::downgrade(&f.shared);
drop(f);
has_finished.recv_timeout(PATIENCE).expect("the dropped work finished");
wait_until_freed(&shared);
let (out, _) = block_on(Interpolator::new(Spline::Linear, Resolution::Seconds).spawn_f64(vec![at(0), at(1)], vec![0.0, 1.0], at(0), at(1)));
assert_eq!(out.expect("runs").len(), 2);
}
#[test]
fn dropping_a_queued_future_cancels_its_work() {
let workers = rayon::current_num_threads();
let (busy, release) = (Arc::new(Barrier::new(workers + 1)), Arc::new(Barrier::new(workers + 1)));
for _ in 0..workers {
let (busy, release) = (Arc::clone(&busy), Arc::clone(&release));
rayon::spawn(move || {
busy.wait();
release.wait();
});
}
busy.wait();
let ran = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&ran);
let f: InterpolationFuture<f64> = InterpolationFuture::spawn(true, move || {
flag.store(true, Ordering::SeqCst);
Err(Error::NoPoints)
});
let shared = Arc::downgrade(&f.shared);
drop(f); release.wait();
wait_until_freed(&shared);
assert!(!ran.load(Ordering::SeqCst), "cancelled work ran");
}
#[test]
fn dropping_started_gpu_work_waits_for_it() {
let (started, has_started) = mpsc::channel();
let finished = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&finished);
let f: InterpolationFuture<f64> = InterpolationFuture::spawn(true, move || {
started.send(()).expect("the test is waiting");
thread::sleep(Duration::from_millis(100));
flag.store(true, Ordering::SeqCst);
Err(Error::NoPoints)
});
has_started.recv_timeout(PATIENCE).expect("the work started");
let (dropped, has_dropped) = mpsc::channel();
thread::spawn(move || {
drop(f);
dropped.send(finished.load(Ordering::SeqCst)).expect("the test is waiting");
});
assert!(has_dropped.recv_timeout(PATIENCE).expect("the drop returned"), "the drop returned before the work finished");
}
#[test]
fn a_drop_on_a_rayon_worker_does_not_wait() {
let (started, has_started) = mpsc::channel();
let (release, released) = mpsc::channel::<()>();
let f: InterpolationFuture<f64> = InterpolationFuture::spawn(true, move || {
started.send(()).expect("the test is waiting");
let _ = released.recv_timeout(PATIENCE);
Err(Error::NoPoints)
});
has_started.recv_timeout(PATIENCE).expect("the work started");
let pool = rayon::ThreadPoolBuilder::new().num_threads(1).build().expect("a pool");
let (dropped, has_dropped) = mpsc::channel();
thread::spawn(move || {
pool.install(move || drop(f));
dropped.send(()).expect("the test is waiting");
});
has_dropped.recv_timeout(PATIENCE).expect("the drop returned while the work was still running");
release.send(()).expect("the job is waiting");
}
#[test]
fn whether_the_gpu_may_compute_a_spawned_job_is_settled_at_spawn() {
let interpolator = Interpolator::new(Spline::Linear, Resolution::Seconds);
assert_eq!(interpolator.backend(Backend::Gpu).for_spawn(), (true, true));
assert_eq!(interpolator.backend(Backend::Parallel).for_spawn(), (true, false));
assert_eq!(interpolator.backend(Backend::Cpu).for_spawn(), (true, false));
let started = crate::gpu::started();
assert_eq!(interpolator.for_spawn(), (started, started));
let _ = crate::prewarm_gpu();
assert_eq!(interpolator.for_spawn(), (cfg!(feature = "gpu"), cfg!(feature = "gpu")));
assert_ne!(crate::auto::choose(usize::MAX, crate::Precision::F64, false), Backend::Gpu);
}
#[test]
fn blocking_on_it_from_a_rayon_worker_does_not_deadlock() {
let pool = rayon::ThreadPoolBuilder::new().num_threads(1).build().expect("a pool");
let (sent, received) = std::sync::mpsc::channel();
thread::spawn(move || {
let out = pool.install(|| block_on(Interpolator::new(Spline::Linear, Resolution::Seconds).spawn_f64(vec![at(0), at(4)], vec![0.0, 4.0], at(0), at(4))));
sent.send(out).expect("the test is waiting");
});
let (out, wakes) = received.recv_timeout(PATIENCE).expect("no deadlock");
assert_eq!(out.expect("runs").len(), 5);
assert_eq!(wakes, 0, "complete on the first poll");
}
}