use std::time::{Duration, Instant};
use pyo3::prelude::*;
use pyo3::types::{PyList, PyTuple};
use pyo3::{PyTraverseError, PyVisit};
use crate::loader::{AsyncSource, DataLoaderIter, LoaderSource, SyncSource};
use crate::python::collator::{PyBatch, PyCollator};
use crate::python::dataloader::PyDataloader;
use crate::python::dataset::{Fetcher, PyAsyncItems, PyDataset, RustSource};
use crate::python::error::into_py_err;
use crate::python::pool::exiting;
use crate::python::transform::{PyItem, PyTransform};
use crate::transform::Transform;
type SyncIter = DataLoaderIter<SyncSource<PyDataset>, PyCollator, PyTransform>;
pub(crate) type AsyncIter<A> = DataLoaderIter<AsyncSource<A>, PyCollator, PyTransform>;
pub(crate) type BatchIndices = std::vec::IntoIter<Vec<usize>>;
const SIGNAL_CHECK_INTERVAL: Duration = Duration::from_millis(100);
pub(crate) enum PyIterInner {
Direct {
chunks: std::vec::IntoIter<Vec<usize>>,
fetcher: Fetcher,
transform: PyTransform,
collator: PyCollator,
},
Threaded(SyncIter),
Async(AsyncIter<PyAsyncItems>),
Rust(AsyncIter<RustSource>),
}
fn wait_next<Src>(
py: Python<'_>,
inner: &mut DataLoaderIter<Src, PyCollator, PyTransform>,
) -> PyResult<Option<PyResult<PyBatch>>>
where
Src: LoaderSource,
PyTransform: Transform<Src::Item, Output = PyItem>,
{
let next = match inner.next_until(None) {
Some(next) => next,
None => loop {
let deadline = Instant::now() + SIGNAL_CHECK_INTERVAL;
if let Some(next) = py.detach(|| inner.next_until(Some(deadline))) {
break next;
}
py.check_signals()?;
},
};
Ok(next.map(|batch| batch.map_err(into_py_err)))
}
const STOP_TIMEOUT: Duration = Duration::from_secs(5);
#[pyclass(name = "PyDataloaderIter", module = "dataloader_rs")]
pub struct PyDataloaderIter {
inner: PyIterInner,
indices: Option<BatchIndices>,
_owner: Py<PyDataloader>,
}
impl Drop for PyDataloaderIter {
fn drop(&mut self) {
if exiting() {
return;
}
Python::try_attach(|py| {
py.detach(|| match &mut self.inner {
PyIterInner::Direct { .. } => true,
PyIterInner::Threaded(inner) => inner.stop(STOP_TIMEOUT),
PyIterInner::Async(inner) => inner.stop(STOP_TIMEOUT),
PyIterInner::Rust(inner) => inner.stop(STOP_TIMEOUT),
})
});
}
}
impl PyDataloaderIter {
pub(crate) fn new(
owner: Py<PyDataloader>,
inner: PyIterInner,
indices: Option<BatchIndices>,
) -> Self {
Self {
inner,
indices,
_owner: owner,
}
}
fn next_batch(&mut self, py: Python<'_>) -> PyResult<Option<PyResult<PyBatch>>> {
match &mut self.inner {
PyIterInner::Direct {
chunks,
fetcher,
transform,
collator,
} => {
let Some(chunk) = chunks.next() else {
return Ok(None);
};
let batch = fetcher.fetch(py, &chunk).and_then(|items| {
let items = items
.into_iter()
.zip(&chunk)
.map(|(item, &index)| transform.call(py, index, item))
.collect::<PyResult<Vec<_>>>()?;
collator.collate_with_py(py, items).map_err(into_py_err)
});
Ok(Some(batch))
}
PyIterInner::Threaded(inner) => wait_next(py, inner),
PyIterInner::Async(inner) => wait_next(py, inner),
PyIterInner::Rust(inner) => wait_next(py, inner),
}
}
}
#[pymethods]
impl PyDataloaderIter {
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
slf
}
fn __next__(&mut self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
let Some(batch) = self.next_batch(py)? else {
return Ok(None);
};
let indices = self.indices.as_mut().map(|indices| indices.next());
let batch = batch?.into_py(py)?;
let Some(indices) = indices else {
return Ok(Some(batch));
};
let indices = PyList::new(py, indices.unwrap_or_default())?;
Ok(Some(
PyTuple::new(py, [batch.into_bound(py), indices.into_any()])?
.into_any()
.unbind(),
))
}
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self._owner)?;
if let PyIterInner::Direct {
fetcher,
transform,
collator,
..
} = &self.inner
{
visit.call(fetcher.method())?;
visit.call(transform.function())?;
visit.call(collator.collate_fn())?;
}
Ok(())
}
fn __len__(&self) -> usize {
match &self.inner {
PyIterInner::Direct { chunks, .. } => chunks.len(),
PyIterInner::Threaded(inner) => inner.len(),
PyIterInner::Async(inner) => inner.len(),
PyIterInner::Rust(inner) => inner.len(),
}
}
}