Skip to main content

hrx/execution/
completion.rs

1use crate::{Error, Result};
2use std::{
3    future::Future,
4    pin::Pin,
5    sync::{
6        Arc, Condvar, Mutex,
7        atomic::{AtomicBool, Ordering},
8    },
9    task::{Context, Poll, Waker},
10    time::{Duration, Instant},
11};
12
13pub(super) struct State {
14    submitted: Option<Instant>,
15    started: Option<Instant>,
16    finished: Option<Instant>,
17    pub done: bool,
18    pub failure: Option<Arc<Error>>,
19    pub cancelled: bool,
20    wakers: [Option<Waker>; 4],
21    overflow_wakers: Vec<Waker>,
22}
23pub(super) struct Signal {
24    pub state: Mutex<State>,
25    changed: Condvar,
26    pub cancel: AtomicBool,
27}
28impl Signal {
29    pub fn new() -> Arc<Self> {
30        Arc::new(Self {
31            state: Mutex::new(State {
32                submitted: None,
33                started: None,
34                finished: None,
35                done: true,
36                failure: None,
37                cancelled: false,
38                wakers: std::array::from_fn(|_| None),
39                overflow_wakers: Vec::new(),
40            }),
41            changed: Condvar::new(),
42            cancel: AtomicBool::new(false),
43        })
44    }
45    pub fn reset(&self) {
46        let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
47        state.done = false;
48        state.submitted = Some(Instant::now());
49        state.started = None;
50        state.finished = None;
51        state.failure = None;
52        state.cancelled = false;
53        state.wakers.fill(None);
54        state.overflow_wakers.clear();
55        self.cancel.store(false, Ordering::Release);
56    }
57    pub fn finish(&self, failure: Option<Arc<Error>>) {
58        let (wakers, overflow) = {
59            let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
60            state.failure = failure;
61            state.cancelled = self.cancel.load(Ordering::Acquire);
62            state.done = true;
63            state.finished = Some(Instant::now());
64            (
65                std::mem::replace(&mut state.wakers, std::array::from_fn(|_| None)),
66                std::mem::take(&mut state.overflow_wakers),
67            )
68        };
69        self.changed.notify_all();
70        for waker in wakers.into_iter().flatten().chain(overflow) {
71            // A task-supplied waker must not take down an execution worker.
72            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| waker.wake()));
73        }
74    }
75}
76impl Signal {
77    pub fn started(&self, now: Instant) {
78        self.state
79            .lock()
80            .unwrap_or_else(|e| e.into_inner())
81            .started
82            .get_or_insert(now);
83    }
84}
85
86/// Host-observed submission latency, including dependency and driver overhead.
87/// This is not a measurement of GPU kernel execution time.
88#[derive(Clone, Copy, Debug, serde::Serialize)]
89pub struct CompletionProfile {
90    /// Time from accepted submission to its terminal result, in microseconds.
91    pub elapsed_us: f64,
92    /// Time until its first native region starts, or None if none ran.
93    pub queue_us: Option<f64>,
94}
95
96fn outcome(state: &State) -> Result<()> {
97    if let Some(message) = &state.failure {
98        return Err(Error::Execution {
99            source: message.clone(),
100        });
101    }
102    if state.cancelled {
103        return Err(Error::Cancelled);
104    }
105    Ok(())
106}
107/// An owned completion observer. Dropping it does not cancel submitted work.
108///
109/// Polling never waits for a device. Clones may be waited on by different tasks.
110#[derive(Clone)]
111pub struct Completion {
112    pub(super) signal: Arc<Signal>,
113    pub(super) core: std::sync::Weak<super::scheduler::Core>,
114}
115impl Completion {
116    /// Host monotonic time at which execution reached a terminal state. This
117    /// permits pipeline boundary timing without including a caller's late wait.
118    /// It is not a device timestamp and does not imply successful execution.
119    pub fn finished_at(&self) -> Option<Instant> {
120        self.signal
121            .state
122            .lock()
123            .unwrap_or_else(|e| e.into_inner())
124            .finished
125    }
126    /// Inspect final host-observed timing without waiting for completion.
127    pub fn profile(&self) -> Option<CompletionProfile> {
128        let state = self.signal.state.lock().unwrap_or_else(|e| e.into_inner());
129        let submitted = state.submitted?;
130        Some(CompletionProfile {
131            elapsed_us: state.finished?.duration_since(submitted).as_secs_f64() * 1e6,
132            queue_us: state
133                .started
134                .map(|start| start.duration_since(submitted).as_secs_f64() * 1e6),
135        })
136    }
137    pub(crate) fn validate_runtime(&self, runtime: &super::Runtime) -> Result<()> {
138        if let Some(owner) = self.core.upgrade() {
139            if !Arc::ptr_eq(&runtime.inner.core, &owner) {
140                return Err(Error::Message(
141                    "completion belongs to another runtime".into(),
142                ));
143            }
144        } else if !self.is_complete() {
145            return Err(Error::Message("orphaned incomplete producer".into()));
146        }
147        Ok(())
148    }
149    /// Observe a terminal result without blocking, or `None` while pending.
150    pub fn result(&self) -> Option<Result<()>> {
151        let state = self.signal.state.lock().unwrap_or_else(|e| e.into_inner());
152        state.done.then(|| outcome(&state))
153    }
154
155    pub(crate) fn ready() -> Self {
156        Self {
157            signal: Signal::new(),
158            core: std::sync::Weak::new(),
159        }
160    }
161
162    /// Whether the operation reached a terminal state, including failure.
163    pub fn is_complete(&self) -> bool {
164        self.signal
165            .state
166            .lock()
167            .unwrap_or_else(|e| e.into_inner())
168            .done
169    }
170    /// Wait for device completion, propagating execution failure.
171    /// The caller may execute ready regions from this submission to avoid a
172    /// worker handoff. Engine limits and all dependency checks still apply.
173    pub fn wait(&self) -> Result<()> {
174        if let Some(core) = self.core.upgrade() {
175            super::scheduler::help(&core, &self.signal);
176        }
177        let mut state = self.signal.state.lock().unwrap_or_else(|e| e.into_inner());
178        while !state.done {
179            state = self
180                .signal
181                .changed
182                .wait(state)
183                .unwrap_or_else(|e| e.into_inner());
184        }
185        outcome(&state)
186    }
187    /// Wait at most `timeout`. `false` leaves execution and ownership intact.
188    pub fn wait_timeout(&self, timeout: Duration) -> Result<bool> {
189        let start = Instant::now();
190        let mut state = self.signal.state.lock().unwrap_or_else(|e| e.into_inner());
191        while !state.done {
192            let Some(left) = timeout.checked_sub(start.elapsed()) else {
193                return Ok(false);
194            };
195            let (next, _) = self
196                .signal
197                .changed
198                .wait_timeout(state, left)
199                .unwrap_or_else(|e| e.into_inner());
200            state = next;
201        }
202        outcome(&state)?;
203        Ok(true)
204    }
205    /// Cancel unscheduled nodes. Running native work is drained before completion.
206    /// This request does not revoke resources or abort an unrelated submission.
207    pub fn cancel(&self) {
208        self.signal.cancel.store(true, Ordering::Release);
209        if let Some(core) = self.core.upgrade() {
210            core.changed.notify_all();
211        }
212    }
213}
214impl Future for Completion {
215    type Output = Result<()>;
216    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
217        let mut state = self.signal.state.lock().unwrap_or_else(|e| e.into_inner());
218        if state.done {
219            return Poll::Ready(outcome(&state));
220        }
221        if !state
222            .wakers
223            .iter()
224            .flatten()
225            .chain(&state.overflow_wakers)
226            .any(|w| w.will_wake(context.waker()))
227        {
228            if let Some(slot) = state.wakers.iter_mut().find(|slot| slot.is_none()) {
229                *slot = Some(context.waker().clone());
230            } else {
231                state.overflow_wakers.push(context.waker().clone());
232            }
233        }
234        Poll::Pending
235    }
236}
237#[cfg(test)]
238mod tests {
239    use super::*;
240    #[test]
241    fn timeout_does_not_cancel_and_detach_retains_state() {
242        let signal = Signal::new();
243        signal.reset();
244        let completion = Completion {
245            signal: signal.clone(),
246            core: std::sync::Weak::new(),
247        };
248        assert!(!completion.wait_timeout(Duration::ZERO).unwrap());
249        assert!(!signal.cancel.load(Ordering::Acquire));
250        drop(completion);
251        signal.finish(None);
252        Completion {
253            signal,
254            core: std::sync::Weak::new(),
255        }
256        .wait()
257        .unwrap();
258    }
259    #[test]
260    fn future_wakeup_and_error() {
261        use std::task::Wake;
262        struct Counter(std::sync::atomic::AtomicUsize);
263        impl Wake for Counter {
264            fn wake(self: Arc<Self>) {
265                self.0.fetch_add(1, Ordering::Relaxed);
266            }
267        }
268        let counter = Arc::new(Counter(std::sync::atomic::AtomicUsize::new(0)));
269        let waker = Waker::from(counter.clone());
270        let mut context = Context::from_waker(&waker);
271        let signal = Signal::new();
272        signal.reset();
273        let mut completion = Completion {
274            signal: signal.clone(),
275            core: std::sync::Weak::new(),
276        };
277        assert!(Pin::new(&mut completion).poll(&mut context).is_pending());
278        signal.finish(Some(Arc::new(Error::DeviceLost(
279            "injected native failure".into(),
280        ))));
281        assert_eq!(counter.0.load(Ordering::Relaxed), 1);
282        assert!(matches!(
283            Pin::new(&mut completion).poll(&mut context),
284            Poll::Ready(Err(Error::Execution { .. }))
285        ));
286    }
287}