use crate::core::{
state::State,
storage::{Real, Scalar, StorageBackend},
};
pub trait Observer<T: Scalar, S: StorageBackend<T>>: Send {
fn observe(&mut self, step: u64, t: f64, state: &State<T, S>);
}
pub trait SnapshotSink<T: Scalar, S: StorageBackend<T>>: Send {
fn consume(&mut self, step: u64, t: f64, state: &State<T, S>);
fn finish(&mut self) {}
}
struct SnapshotMsg<T: Scalar, S: StorageBackend<T>> {
step: u64,
t: f64,
state: State<T, S>,
}
pub struct AsyncObserver<T: Scalar, S: StorageBackend<T> + 'static> {
every: u64,
work_tx: Option<tokio::sync::mpsc::Sender<SnapshotMsg<T, S>>>,
free_rx: tokio::sync::mpsc::Receiver<State<T, S>>,
worker: Option<std::thread::JoinHandle<()>>,
dropped: u64,
}
impl<T: Real, S: StorageBackend<T> + 'static> AsyncObserver<T, S> {
#[must_use]
pub fn new(
every: u64,
buffers: Vec<State<T, S>>,
mut sinks: Vec<Box<dyn SnapshotSink<T, S>>>,
) -> Self {
let depth = buffers.len().max(1);
let (work_tx, mut work_rx) = tokio::sync::mpsc::channel::<SnapshotMsg<T, S>>(depth);
let (free_tx, free_rx) = tokio::sync::mpsc::channel::<State<T, S>>(depth);
for buf in buffers {
free_tx
.try_send(buf)
.unwrap_or_else(|_| unreachable!("free ring sized to buffer count"));
}
let worker = std::thread::Builder::new()
.name("legendre-observer".into())
.spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.expect("observer runtime");
rt.block_on(async move {
while let Some(msg) = work_rx.recv().await {
for sink in &mut sinks {
sink.consume(msg.step, msg.t, &msg.state);
}
let _ = free_tx.send(msg.state).await;
}
for sink in &mut sinks {
sink.finish();
}
});
})
.expect("spawning observer thread");
Self {
every,
work_tx: Some(work_tx),
free_rx,
worker: Some(worker),
dropped: 0,
}
}
#[must_use]
pub const fn dropped_snapshots(&self) -> u64 {
self.dropped
}
}
impl<T: Real, S: StorageBackend<T> + 'static> Observer<T, S> for AsyncObserver<T, S> {
fn observe(&mut self, step: u64, t: f64, state: &State<T, S>) {
if step != 1 && !step.is_multiple_of(self.every) {
return;
}
let Ok(mut buf) = self.free_rx.try_recv() else {
self.dropped += 1;
return;
};
buf.copy_from(state);
let msg = SnapshotMsg {
step,
t,
state: buf,
};
if let Some(tx) = &self.work_tx
&& tx.try_send(msg).is_err()
{
self.dropped += 1;
}
}
}
impl<T: Scalar, S: StorageBackend<T> + 'static> Drop for AsyncObserver<T, S> {
fn drop(&mut self) {
self.work_tx.take();
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
if self.dropped > 0 {
tracing::warn!(
dropped = self.dropped,
"observer ring was saturated; snapshots were skipped"
);
}
}
}