Skip to main content

ax_task/sched/system/task_system/
deferred_work.rs

1//! Deferred task-context work and resource reclamation.
2
3use super::*;
4use crate::runtime::service::{SchedulerTickMode, SchedulerTickWorkDisposition};
5
6#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7struct SchedulerTickDispatch {
8    events: usize,
9    callbacks: usize,
10    retry_deferred: bool,
11}
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14enum ResourceReclaim {
15    None,
16    Coroutine,
17    AddressSpace,
18}
19
20impl TaskSystem {
21    pub(crate) fn publish_current_scheduler_tick_work(
22        &self,
23        cpu: &CpuLocal,
24        expected: ThreadId,
25        observed_ns: u64,
26        mode: SchedulerTickMode,
27        tick_ns: u64,
28    ) -> Result<(), TaskError> {
29        let Some(core) = cpu.current_core() else {
30            return Err(TaskError::NoRunnableThread);
31        };
32        if core.id() != expected {
33            return Err(TaskError::StaleThreadId);
34        }
35        core.sample_scheduler_tick_cpu_time(mode, tick_ns);
36        self.publish_scheduler_tick_work(&core, observed_ns);
37        Ok(())
38    }
39
40    fn publish_scheduler_tick_work(&self, core: &Arc<ThreadCore>, observed_ns: u64) {
41        if !core.begin_scheduler_tick_work(observed_ns) {
42            return;
43        }
44        self.publish_claimed_scheduler_tick_work(core);
45    }
46
47    fn publish_claimed_scheduler_tick_work(&self, core: &Arc<ThreadCore>) {
48        if !core.reserve_scheduler_inbox_delivery() {
49            core.cancel_scheduler_tick_work();
50            return;
51        }
52
53        let core_ptr = Arc::as_ptr(core);
54        // SAFETY: the retained strong count is transferred to the task-work
55        // inbox and released by its sole consumer after callback completion.
56        unsafe { Arc::increment_strong_count(core_ptr) };
57        // SAFETY: Arc allocations are pinned for their lifetime, and the
58        // delivery reservation keeps this core and its extension alive.
59        let node = unsafe { Pin::new_unchecked((&*core_ptr).scheduler_tick_work_node()) };
60        let result = self.deferred_scheduler_ticks.publish(
61            node,
62            InboxMessage::scheduler_tick(core.id(), core_ptr.expose_provenance()),
63        );
64        if result == PublishResult::Published {
65            self.task_work.publish();
66            return;
67        }
68
69        // SAFETY: a rejected publication did not consume the transferred Arc.
70        unsafe { Arc::decrement_strong_count(core_ptr) };
71        core.cancel_scheduler_inbox_delivery();
72        core.cancel_scheduler_tick_work();
73        task_runtime::fatal_invariant(0x5457_0001, result as usize);
74    }
75
76    /// Publishes one zero-reference coroutine header from hard IRQ context.
77    pub(crate) fn publish_deferred_coroutine_reclaim(
78        &self,
79        header: Pin<&'static CoroutineHeader>,
80    ) -> PublishResult {
81        let data = header.address();
82        let _publication = IrqScope::enter();
83        let result = self.deferred_coroutine_reclaims.publish(
84            header.reclaim_node(),
85            InboxMessage::reclaim(ThreadId::from_parts(0, 0), 0, data),
86        );
87        if result == PublishResult::Published {
88            self.task_work.publish();
89        }
90        result
91    }
92
93    pub(crate) fn task_work_doorbell(&self) -> Arc<TaskWorkDoorbell> {
94        Arc::clone(&self.task_work)
95    }
96
97    pub(crate) fn begin_task_work_worker_install(&self) -> Result<(), TaskError> {
98        self.task_work.begin_worker_install()
99    }
100
101    pub(crate) fn finish_task_work_worker_install(&self) {
102        self.task_work.finish_worker_install();
103    }
104
105    pub(crate) fn cancel_task_work_worker_install(&self) {
106        self.task_work.cancel_worker_install();
107    }
108
109    /// Reports whether a sticky task-work publication awaits the service thread.
110    pub fn deferred_task_work_pending(&self) -> bool {
111        self.task_work.is_pending()
112    }
113
114    pub(crate) fn publish_resource_release_ready(&self) {
115        self.task_work.publish();
116    }
117
118    /// Runs one bounded task-context pass as the single task-work consumer.
119    ///
120    /// Unrelated work classes are interleaved through a persistent round-robin
121    /// cursor. Per-thread claim predicates still enforce Deadline callback,
122    /// exit callback, and record-reaping order. A concurrent or reentrant
123    /// consumer receives [`TaskError::ThreadBusy`] without consuming work.
124    pub fn service_deferred_task_work(
125        &self,
126        limit: usize,
127    ) -> Result<DeferredTaskWorkBatch, TaskError> {
128        if task_runtime::in_hard_irq() {
129            return Err(TaskError::UnsafeContext);
130        }
131        let limit = limit.min(crate::runtime::config::DEFAULT_BATCH_LIMIT);
132        if limit == 0 {
133            return Ok(DeferredTaskWorkBatch::default());
134        }
135        let _consumer: TaskWorkConsumerGuard<'_> = self.task_work.try_claim_consumer()?;
136        let mut next_class = self.state.lock().task_work_class_cursor;
137        let outcome = (|| {
138            let mut batch = DeferredTaskWorkBatch::default();
139            let mut classes_without_progress = 0;
140            let mut scheduler_tick_retry_deferred = false;
141            while batch.processed() < limit
142                && classes_without_progress < DeferredTaskWorkClass::COUNT
143            {
144                let class = next_class;
145                next_class = class.next();
146                let processed = match class {
147                    DeferredTaskWorkClass::Deadline => {
148                        let (events, callbacks) = self.dispatch_deadline_overruns_inner(1)?;
149                        batch.deadline_events += events;
150                        batch.deadline_callbacks += callbacks;
151                        events
152                    }
153                    DeferredTaskWorkClass::SchedulerTick if scheduler_tick_retry_deferred => 0,
154                    DeferredTaskWorkClass::SchedulerTick => {
155                        let dispatch = self.dispatch_scheduler_tick_work_inner(1)?;
156                        batch.scheduler_tick_events += dispatch.events;
157                        batch.scheduler_tick_callbacks += dispatch.callbacks;
158                        scheduler_tick_retry_deferred |= dispatch.retry_deferred;
159                        dispatch.events
160                    }
161                    DeferredTaskWorkClass::Exit => {
162                        let callbacks = self.dispatch_exit_callbacks_inner(1)?;
163                        batch.exit_callbacks += callbacks;
164                        callbacks
165                    }
166                    DeferredTaskWorkClass::Reap => {
167                        let reaped = self.reap_unreferenced_exited_inner(1)?;
168                        batch.reaped_threads += reaped;
169                        reaped
170                    }
171                    DeferredTaskWorkClass::Reclaim => match self.reclaim_one_resource()? {
172                        ResourceReclaim::None => 0,
173                        ResourceReclaim::Coroutine => {
174                            batch.coroutine_reclaims += 1;
175                            1
176                        }
177                        ResourceReclaim::AddressSpace => {
178                            batch.address_space_reclaims += 1;
179                            1
180                        }
181                    },
182                };
183                if processed == 0 {
184                    classes_without_progress += 1;
185                } else {
186                    classes_without_progress = 0;
187                }
188            }
189            debug_assert!(batch.processed() <= limit);
190            #[cfg(feature = "qperf-metrics")]
191            crate::diagnostics::counters::record_task_work_classes(
192                batch.deadline_events,
193                batch.scheduler_tick_events,
194                batch.exit_callbacks,
195                batch.reaped_threads,
196                batch.coroutine_reclaims,
197                batch.address_space_reclaims,
198            );
199            Ok(batch)
200        })();
201        self.state.lock().task_work_class_cursor = next_class;
202        outcome
203    }
204
205    fn dispatch_scheduler_tick_work_inner(
206        &self,
207        limit: usize,
208    ) -> Result<SchedulerTickDispatch, TaskError> {
209        let mut messages = [InboxMessage::EMPTY; crate::runtime::config::DEFAULT_BATCH_LIMIT];
210        let batch = self.deferred_scheduler_ticks.drain(
211            limit.min(crate::runtime::config::DEFAULT_BATCH_LIMIT),
212            &mut messages,
213        );
214        let mut callbacks = 0;
215        let mut retry_deferred = false;
216        for message in messages.iter().take(batch.drained()) {
217            if message.operation() != InboxOperation::SchedulerTick || message.payload() == 0 {
218                task_runtime::fatal_invariant(0x5457_0002, message.payload());
219            }
220            let core = unsafe {
221                // SAFETY: publication transferred exactly one Arc strong count
222                // whose pointer is carried by this detached message.
223                Arc::from_raw(ptr::with_exposed_provenance::<ThreadCore>(
224                    message.payload(),
225                ))
226            };
227            let _delivery = core.accept_scheduler_inbox_delivery();
228            if core.id() != message.thread_id() {
229                core.cancel_scheduler_tick_work();
230                continue;
231            }
232            let claim = core.take_scheduler_tick_work();
233            if let Some(claim) = claim
234                && let Some(extension) = core.extension_view()
235            {
236                // SAFETY: the inbox delivery reservation prevents extension
237                // reclamation even if the carrier thread exits concurrently.
238                // The gate generation decides whether the process/subsystem
239                // work remains relevant; carrier-thread state does not.
240                let disposition = unsafe { claim.invoke(extension.data(), core.id()) };
241                callbacks += 1;
242                if disposition == SchedulerTickWorkDisposition::Retry {
243                    retry_deferred = true;
244                    if core.retry_scheduler_tick_work(&claim) {
245                        self.publish_claimed_scheduler_tick_work(&core);
246                    }
247                }
248            }
249        }
250        if batch.pending() {
251            self.task_work.publish();
252        }
253        Ok(SchedulerTickDispatch {
254            events: batch.drained(),
255            callbacks,
256            retry_deferred,
257        })
258    }
259
260    fn reclaim_one_resource(&self) -> Result<ResourceReclaim, TaskError> {
261        let address_space_reclaim_first = {
262            let mut state = self.state.lock();
263            let current = state.address_space_reclaim_first;
264            state.address_space_reclaim_first = !current;
265            current
266        };
267        if address_space_reclaim_first {
268            if self.reclaim_pending_address_space() {
269                return Ok(ResourceReclaim::AddressSpace);
270            }
271            self.drain_deferred_coroutine_reclaims_inner(1)
272                .map(|count| match count {
273                    0 => ResourceReclaim::None,
274                    1 => ResourceReclaim::Coroutine,
275                    _ => unreachable!("single-resource drain exceeded its bound"),
276                })
277        } else {
278            let reclaimed = self.drain_deferred_coroutine_reclaims_inner(1)?;
279            if reclaimed != 0 {
280                Ok(ResourceReclaim::Coroutine)
281            } else if self.reclaim_pending_address_space() {
282                Ok(ResourceReclaim::AddressSpace)
283            } else {
284                Ok(ResourceReclaim::None)
285            }
286        }
287    }
288
289    fn reclaim_pending_address_space(&self) -> bool {
290        let Some(address_space) = self.state.lock().pending_address_space_reclaims.pop() else {
291            return false;
292        };
293        let handle = address_space.handle();
294        match task_runtime::destroy_address_space(handle) {
295            AddressSpaceDestroyOutcome::Released => {}
296            AddressSpaceDestroyOutcome::Active => {
297                self.state
298                    .lock()
299                    .pending_address_space_reclaims
300                    .push(address_space);
301                match task_runtime::arm_address_space_reclaim(handle) {
302                    AddressSpaceReclaimArmOutcome::Ready => self.task_work.publish(),
303                    AddressSpaceReclaimArmOutcome::Armed => {}
304                }
305                return false;
306            }
307        }
308        true
309    }
310
311    fn drain_deferred_coroutine_reclaims_inner(&self, limit: usize) -> Result<usize, TaskError> {
312        const MAX_DRAIN_BATCH: usize = 64;
313
314        let mut messages = [InboxMessage::EMPTY; MAX_DRAIN_BATCH];
315        let batch = self
316            .deferred_coroutine_reclaims
317            .drain(limit.min(MAX_DRAIN_BATCH), &mut messages);
318        for message in messages.iter().take(batch.drained()) {
319            if message.operation() != InboxOperation::Reclaim || message.payload() == 0 {
320                task_runtime::fatal_invariant(0x4558_0009, message.payload());
321            }
322            let header = ptr::with_exposed_provenance_mut::<CoroutineHeader>(message.payload());
323            unsafe {
324                // Detachment cleared the embedded reclaim membership. Zero
325                // references and FUTURE_EMPTY make the type-erased allocation
326                // exclusively owned by this task-context consumer.
327                CoroutineHeader::deallocate_raw(header);
328            }
329        }
330        Ok(batch.drained())
331    }
332}