use std::pin::Pin;
use std::sync::Arc;
use futures_core::Stream;
use futures_util::stream::{self, StreamExt};
use webdataset_core::error::Result;
use webdataset_core::sample::Sample;
use crate::pipeline::{Repetitions, Stage};
pub type SampleStream = Pin<Box<dyn Stream<Item = Result<Sample>> + Send>>;
pub trait AsyncStage: Send + Sync + std::fmt::Debug {
fn apply(&self, input: SampleStream) -> SampleStream;
}
pub fn empty_stream() -> SampleStream {
Box::pin(stream::empty())
}
pub struct FnStage<F> {
name: &'static str,
apply: F,
}
impl<F> std::fmt::Debug for FnStage<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name)
}
}
impl<F> AsyncStage for FnStage<F>
where
F: Fn(SampleStream) -> SampleStream + Send + Sync,
{
fn apply(&self, input: SampleStream) -> SampleStream {
(self.apply)(input)
}
}
pub fn stage_fn<F>(name: &'static str, apply: F) -> FnStage<F>
where
F: Fn(SampleStream) -> SampleStream + Send + Sync,
{
FnStage { name, apply }
}
#[derive(Debug, Clone)]
pub struct LiftSource(Arc<dyn Stage>);
impl LiftSource {
pub fn new(stage: impl Stage + 'static) -> LiftSource {
LiftSource(Arc::new(stage))
}
pub fn shared(stage: Arc<dyn Stage>) -> LiftSource {
LiftSource(stage)
}
}
impl AsyncStage for LiftSource {
fn apply(&self, _input: SampleStream) -> SampleStream {
Box::pin(stream::iter(self.0.apply(crate::pipeline::empty_stream())))
}
}
#[derive(Debug, Clone, Default)]
pub struct AsyncDataPipeline {
stages: Vec<Arc<dyn AsyncStage>>,
repetitions: Repetitions,
limit: Option<usize>,
}
impl AsyncDataPipeline {
pub fn new() -> AsyncDataPipeline {
AsyncDataPipeline::default()
}
pub fn with(mut self, stage: impl AsyncStage + 'static) -> AsyncDataPipeline {
self.push(stage);
self
}
pub fn with_source(self, stage: impl Stage + 'static) -> AsyncDataPipeline {
self.with(LiftSource::new(stage))
}
pub fn push(&mut self, stage: impl AsyncStage + 'static) {
self.stages.push(Arc::new(stage));
}
pub fn push_shared(&mut self, stage: Arc<dyn AsyncStage>) {
self.stages.push(stage);
}
pub fn len(&self) -> usize {
self.stages.len()
}
pub fn is_empty(&self) -> bool {
self.stages.is_empty()
}
pub fn repeat(mut self, epochs: usize) -> AsyncDataPipeline {
self.repetitions = Repetitions::Times(epochs);
self
}
pub fn repeat_forever(mut self) -> AsyncDataPipeline {
self.repetitions = Repetitions::Forever;
self
}
pub fn with_epoch(mut self, nsamples: usize) -> AsyncDataPipeline {
self.repetitions = Repetitions::Forever;
self.limit = Some(nsamples);
self
}
pub fn take(mut self, nsamples: usize) -> AsyncDataPipeline {
self.limit = Some(nsamples);
self
}
fn run_once(&self) -> SampleStream {
let mut stream = empty_stream();
for stage in &self.stages {
stream = stage.apply(stream);
}
stream
}
pub fn stream(&self) -> SampleStream {
let stream = match self.repetitions {
Repetitions::Once => self.run_once(),
_ => self.repeated(),
};
match self.limit {
Some(n) => Box::pin(stream.take(n)),
None => stream,
}
}
fn repeated(&self) -> SampleStream {
let epochs_left = match self.repetitions {
Repetitions::Once => Some(1),
Repetitions::Times(n) => Some(n),
Repetitions::Forever => None,
};
struct State {
pipeline: AsyncDataPipeline,
current: SampleStream,
epochs_left: Option<usize>,
produced: usize,
}
let state = State {
current: self.run_once(),
pipeline: self.clone(),
epochs_left: epochs_left.map(|n| n.saturating_sub(1)),
produced: 0,
};
Box::pin(stream::unfold(Some(state), |carried| async move {
let mut state = carried?;
loop {
if let Some(item) = state.current.next().await {
state.produced += 1;
return Some((item, Some(state)));
}
if state.produced == 0 {
return None;
}
match state.epochs_left {
Some(0) => return None,
Some(n) => state.epochs_left = Some(n - 1),
None => {}
}
state.current = state.pipeline.run_once();
state.produced = 0;
}
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::Samples;
use futures_executor::block_on;
use futures_util::TryStreamExt;
use webdataset_core::value::Value;
fn three() -> Samples {
Samples::new((0..3).map(|i| {
let mut s = Sample::with_key(format!("k{i}"));
s.insert("cls", Value::Int(i));
s
}))
}
fn keys(pipeline: &AsyncDataPipeline) -> Vec<String> {
block_on(async {
pipeline
.stream()
.try_collect::<Vec<_>>()
.await
.expect("no failures")
.into_iter()
.map(|s| s.key().expect("a key").to_string())
.collect()
})
}
#[test]
fn lifts_a_blocking_source() {
let pipeline = AsyncDataPipeline::new().with_source(three());
assert_eq!(keys(&pipeline), ["k0", "k1", "k2"]);
}
#[test]
fn runs_the_stages_in_order() {
let pipeline = AsyncDataPipeline::new().with_source(three()).with(stage_fn("double", |input| {
Box::pin(input.map(|item| {
item.map(|mut s| {
let doubled = s.get("cls").and_then(Value::as_i64).unwrap_or(0) * 2;
s.insert("cls", Value::Int(doubled));
s
})
}))
}));
let values: Vec<i64> = block_on(async {
pipeline
.stream()
.try_collect::<Vec<_>>()
.await
.expect("no failures")
.iter()
.map(|s| s.get("cls").and_then(Value::as_i64).expect("cls"))
.collect()
});
assert_eq!(values, [0, 2, 4]);
}
#[test]
fn replays_the_source_on_every_stream() {
let pipeline = AsyncDataPipeline::new().with_source(three());
assert_eq!(keys(&pipeline).len(), 3);
assert_eq!(keys(&pipeline).len(), 3);
}
#[test]
fn repeats_a_fixed_number_of_epochs() {
let pipeline = AsyncDataPipeline::new().with_source(three()).repeat(3);
assert_eq!(keys(&pipeline).len(), 9);
}
#[test]
fn redraws_epoch_boundaries() {
let pipeline = AsyncDataPipeline::new().with_source(three()).with_epoch(7);
let seen = keys(&pipeline);
assert_eq!(seen.len(), 7);
assert_eq!(seen[3], "k0", "the source restarts once exhausted");
}
#[test]
fn stops_instead_of_spinning_on_an_empty_source() {
let pipeline = AsyncDataPipeline::new().with_source(Samples::new([])).with_epoch(10);
assert_eq!(keys(&pipeline).len(), 0);
}
#[test]
fn truncates_with_take() {
let pipeline = AsyncDataPipeline::new().with_source(three()).take(2);
assert_eq!(keys(&pipeline).len(), 2);
}
}