Skip to main content

dope_fiber/task/batch/
join.rs

1use core::{convert, pin, task::Poll};
2
3use o3::depot::array;
4
5use crate::{
6    abi, context,
7    core::driver::schedule::{
8        app,
9        ready::{
10            task,
11            task::{pool, topology},
12        },
13    },
14    task::{
15        batch::{Completed, Error, Index, Pool, RootError, Tag, state},
16        storage::slots,
17    },
18};
19
20const LIMIT: usize = 32;
21
22enum Progress<const N: usize> {
23    Done(Completed<N>),
24    More,
25    Parked,
26}
27
28/// One ordered, fixed-capacity group of homogeneous fibers.
29#[pin_project::pin_project(PinnedDrop)]
30#[must_use = "a fiber does nothing unless it is driven"]
31pub struct Join<'pool, 'd, F, const N: usize>
32where
33    F: abi::Fiber<'d>,
34{
35    #[pin]
36    table: slots::Table<'d, F, F::Output, N>,
37    pool: &'pool Pool<'d, N>,
38    state: state::State<N>,
39    root: Option<pool::Root<'d>>,
40}
41
42impl<'pool, 'd, F, const N: usize> Join<'pool, 'd, F, N>
43where
44    F: abi::Fiber<'d>,
45{
46    /// Builds an empty fixed-capacity batch.
47    pub fn new(pool: &'pool mut Pool<'d, N>) -> Self {
48        Self::from_table(pool, slots::Table::vacant(), array::Len::ZERO)
49    }
50
51    /// Builds a batch containing every fiber in array order.
52    pub fn from_array(pool: &'pool mut Pool<'d, N>, fibers: [F; N]) -> Self {
53        Self::from_map(pool, fibers, convert::identity)
54    }
55
56    /// Consumes each input in array order, constructing every fiber before returning.
57    pub fn from_map<P>(
58        pool: &'pool mut Pool<'d, N>,
59        input: [P; N],
60        make: impl FnMut(P) -> F,
61    ) -> Self {
62        Self::from_table(pool, slots::Table::from_map(input, make), array::Len::MAX)
63    }
64
65    /// Builds a full batch without materializing an intermediate fiber array.
66    pub fn from_fn(pool: &'pool mut Pool<'d, N>, make: impl FnMut(Index<N>) -> F) -> Self {
67        Self::from_table(pool, slots::Table::from_fn(make), array::Len::MAX)
68    }
69
70    fn from_table(
71        pool: &'pool mut Pool<'d, N>,
72        table: slots::Table<'d, F, F::Output, N>,
73        len: array::Len<N>,
74    ) -> Self {
75        Self {
76            table,
77            pool,
78            state: state::State::new(len),
79            root: None,
80        }
81    }
82
83    pub fn try_push(&mut self, fiber: F) -> Result<(), F> {
84        let Some(state) = self.state.vacancy() else {
85            return Err(fiber);
86        };
87        let index = slots::Index::from_vacant(state.slot());
88        // SAFETY: only this owner pairs its table and admitted prefix. Empty
89        // construction initializes every entry as vacant; full construction has
90        // no vacancy. Each insertion fills exactly the next entry and commits
91        // that same prefix. This exclusive borrow cannot overlap a pinned poll,
92        // cancellation, or another insertion.
93        let entry = unsafe { slots::raw::Vacant::new(&mut self.table, index) };
94        entry.insert(fiber);
95        state.commit();
96        Ok(())
97    }
98
99    fn progress(
100        self: pin::Pin<&mut Self>,
101        mut context: pin::Pin<&mut context::Context<'_, 'd>>,
102    ) -> Result<Progress<N>, Error> {
103        let mut this = self.project();
104        if let Some(done) = this.state.completed() {
105            return Ok(Progress::Done(done));
106        }
107
108        let pool = *this.pool;
109        let root = pool
110            .tasks
111            .try_root(context.as_ref().waker().0)
112            .map_err(|error| Error::Root(RootError::Invalid(error)))?;
113        match *this.root {
114            None => *this.root = Some(root),
115            Some(active) if active == root => {}
116            Some(active) => {
117                return Err(Error::Root(RootError::Changed {
118                    active: active.id(),
119                    observed: root.id(),
120                }));
121            }
122        }
123
124        let mut budget = LIMIT;
125        let mut initial = true;
126        while budget != 0 {
127            let Some(fresh) = this.state.fresh() else {
128                break;
129            };
130            let index = slots::Index::from_vacant(fresh.slot());
131            let permit = if initial {
132                initial = false;
133                None
134            } else {
135                match context.as_ref().try_continue() {
136                    context::Continue::Permit(permit) => Some(permit),
137                    context::Continue::Exhausted => return Ok(Progress::More),
138                }
139            };
140            let attach = pool.ready.attach(index);
141            let done = poll_slot(
142                this.table.as_mut(),
143                context.as_mut(),
144                index,
145                attach,
146                permit,
147                &pool.tasks,
148                root,
149            )
150            .map_err(Error::Task)?;
151            budget -= 1;
152            if let state::Status::Done(all) = fresh.commit(done) {
153                return Ok(Progress::Done(all));
154            }
155        }
156
157        if budget == 0 {
158            if this.state.has_fresh() {
159                return Ok(Progress::More);
160            }
161        } else {
162            let Some(mut drain) = pool.ready.drain() else {
163                return Ok(Progress::Parked);
164            };
165            loop {
166                let pending = match this.state.pending() {
167                    state::PendingState::Live(pending) => pending,
168                    state::PendingState::Done(done) => return Ok(Progress::Done(done)),
169                };
170                let (index, permit) = if initial {
171                    let Some(index) = drain.pop() else {
172                        break;
173                    };
174                    initial = false;
175                    (index, None)
176                } else {
177                    match context.as_ref().work().next(&mut drain, &mut ()) {
178                        app::Next::Item(index, permit) => (index, Some(permit)),
179                        app::Next::Empty => break,
180                        app::Next::Exhausted(_) => return Ok(Progress::More),
181                    }
182                };
183                let attach = pool.ready.attach(index);
184                budget -= 1;
185                let done = poll_slot(
186                    this.table.as_mut(),
187                    context.as_mut(),
188                    index,
189                    attach,
190                    permit,
191                    &pool.tasks,
192                    root,
193                )
194                .map_err(Error::Task)?;
195                if let state::Status::Done(all) = pending.commit(done) {
196                    return Ok(Progress::Done(all));
197                }
198                if budget == 0 {
199                    break;
200                }
201            }
202        }
203        Ok(if pool.ready.is_empty() {
204            Progress::Parked
205        } else {
206            Progress::More
207        })
208    }
209
210    fn cancel(self: pin::Pin<&mut Self>) {
211        let mut this = self.project();
212        if let Some(root) = this.root.take() {
213            let pool = *this.pool;
214            for index in slots::Index::until(this.state.next()) {
215                this.table.as_mut().cancel(index, |bindings| {
216                    bindings.reclaim_pool(index, &pool.tasks, root);
217                });
218            }
219        }
220    }
221}
222
223fn poll_slot<'turn, 'd: 'turn, F, const N: usize>(
224    mut table: pin::Pin<&mut slots::Table<'d, F, F::Output, N>>,
225    mut context: pin::Pin<&mut context::Context<'turn, 'd>>,
226    index: slots::Index<N>,
227    edge: topology::Edge<'_, 'd, slots::Index<N>>,
228    permit: Option<app::Permit<'turn, 'd>>,
229    pool: &pool::Pool<'d, Tag, N>,
230    root: pool::Root<'d>,
231) -> Result<state::Completion, task::Error>
232where
233    F: abi::Fiber<'d>,
234{
235    match table.as_mut().visit(index, (), |fiber, bindings, ()| {
236        let poll = bindings.poll_pool(edge, pool, root, |wake| {
237            match permit {
238                Some(permit) => unsafe {
239                    context::raw::Live::new(fiber).child_with(context.as_mut(), wake, permit)
240                },
241                None => {
242                    // SAFETY: progress consumes its original PollCall admission
243                    // for exactly the first live child; later children carry permits.
244                    // Table::visit ends this child's live state on Ready.
245                    unsafe { context::raw::Live::new(fiber).child(context.as_mut(), wake) }
246                }
247            }
248        });
249        let poll = match poll {
250            Ok(poll) => poll,
251            Err(error) => return slots::Action::Keep(Err(error)),
252        };
253        let Poll::Ready(output) = poll else {
254            return slots::Action::Keep(Ok(state::Completion::Pending));
255        };
256        slots::Action::Complete(output, Ok(state::Completion::Ready))
257    }) {
258        slots::Phase::Live(result) => result,
259        slots::Phase::Vacant(()) | slots::Phase::Done(()) => Ok(state::Completion::Pending),
260    }
261}
262
263impl<'pool, 'd, F, const N: usize> abi::Fiber<'d> for Join<'pool, 'd, F, N>
264where
265    F: abi::Fiber<'d>,
266{
267    type Output = Result<array::IntoIter<F::Output, N>, Error>;
268
269    fn poll(call: context::PollCall<'_, '_, 'd, Self>) -> Poll<Self::Output> {
270        let (mut this, mut context) = call.into_parts();
271        match this.as_mut().progress(context.as_mut()) {
272            Ok(Progress::Done(done)) => {
273                let output = this.as_mut().project().table.drain_exact(done);
274                *this.as_mut().project().root = None;
275                Poll::Ready(Ok(output))
276            }
277            Ok(Progress::Parked) => Poll::Pending,
278            Ok(Progress::More) => {
279                context.requeue();
280                Poll::Pending
281            }
282            Err(error) => {
283                this.as_mut().cancel();
284                Poll::Ready(Err(error))
285            }
286        }
287    }
288}
289
290#[pin_project::pinned_drop]
291impl<'d, F, const N: usize> PinnedDrop for Join<'_, 'd, F, N>
292where
293    F: abi::Fiber<'d>,
294{
295    fn drop(self: pin::Pin<&mut Self>) {
296        self.cancel();
297    }
298}
299
300const _: () = {
301    use core::mem;
302
303    let word = mem::size_of::<usize>();
304    assert!(mem::size_of::<Join<'static, 'static, abi::Ready<()>, 0>>() == 4 * word);
305    assert!(mem::align_of::<Join<'static, 'static, abi::Ready<()>, 0>>() == word);
306    assert!(mem::size_of::<Option<Join<'static, 'static, abi::Ready<()>, 0>>>() == 4 * word);
307};