moirai_executor/schedule/runtime/
worker.rs1use std::{
4 sync::{Mutex, MutexGuard},
5 thread,
6};
7
8use moirai_core::error::{ExecutorError, ExecutorResult};
9
10use super::super::job::ScheduledJob;
11use super::super::queue::WorkerQueueOwner;
12
13use super::types::{set_current_worker_id, ContendedWakePolicy, SchedulerInner, WorkerState};
14
15#[cfg(feature = "scheduler-diagnostics")]
16use super::types::BoundedContendedWake;
17
18pub(super) const WORKER_IDLE_SPIN_ATTEMPTS: usize = 256;
19pub(super) const JOIN_FAST_SPIN_ATTEMPTS: usize = WORKER_IDLE_SPIN_ATTEMPTS;
20
21pub(super) fn worker_loop<const QUEUE_CAPACITY: usize, const SPIN_LIMIT: usize>(
22 inner: std::sync::Arc<SchedulerInner<QUEUE_CAPACITY>>,
23 worker_id: usize,
24 mut owner: WorkerQueueOwner<QUEUE_CAPACITY>,
25) {
26 set_current_worker_id(Some(worker_id));
27 let _ = inner.workers[worker_id].thread.set(thread::current());
28
29 loop {
30 if let Some(job) = next_job(&inner, worker_id, &mut owner) {
31 execute_job(&inner, worker_id, job);
32 continue;
33 }
34
35 if should_stop(&inner) {
36 break;
37 }
38
39 if spin_for_work::<QUEUE_CAPACITY, SPIN_LIMIT>(&inner, worker_id) {
40 continue;
41 }
42
43 run_idle_memory_maintenance();
46
47 wait_for_work(&inner, worker_id);
48 }
49}
50
51#[cfg(feature = "mnemosyne")]
52melinoe::thread_cached! {
53 mod last_maintenance_time: std::time::Instant;
54}
55
56#[inline]
57fn run_idle_memory_maintenance() {
58 #[cfg(feature = "mnemosyne")]
59 {
60 use mnemosyne::{LocalAllocatorSelector, MemoryBackendWrapper, StandardPolicy};
61 if <MemoryBackendWrapper as LocalAllocatorSelector<MemoryBackendWrapper>>::get_allocator_ptr_raw().is_null() {
62 return;
63 }
64
65 let now = std::time::Instant::now();
66 let should_run = if let Some(last) = last_maintenance_time::get() {
67 if now.duration_since(last) >= std::time::Duration::from_millis(500) {
68 last_maintenance_time::set(now);
69 true
70 } else {
71 false
72 }
73 } else {
74 last_maintenance_time::set(now);
75 true
76 };
77
78 if should_run {
79 let _ =
80 <MemoryBackendWrapper as LocalAllocatorSelector<MemoryBackendWrapper>>::with_allocator(
81 |alloc| unsafe {
82 alloc.periodic_defragmentation_sweep::<StandardPolicy>();
83 },
84 );
85 }
86 }
87}
88
89pub(super) fn next_job<const QUEUE_CAPACITY: usize>(
90 inner: &SchedulerInner<QUEUE_CAPACITY>,
91 worker_id: usize,
92 owner: &mut WorkerQueueOwner<QUEUE_CAPACITY>,
93) -> Option<ScheduledJob> {
94 let local = &inner.workers[worker_id];
95 local
96 .lifo_slot
97 .pop()
98 .or_else(|| owner.pop_local())
99 .or_else(|| steal_job(inner, worker_id, owner))
100}
101
102pub(super) fn next_shared_job<const QUEUE_CAPACITY: usize>(
104 inner: &SchedulerInner<QUEUE_CAPACITY>,
105 worker_id: usize,
106) -> Option<ScheduledJob> {
107 let local = &inner.workers[worker_id];
108 local
109 .lifo_slot
110 .pop()
111 .or_else(|| local.queues.steal_one())
112 .or_else(|| steal_shared_job(inner, worker_id))
113}
114
115fn steal_job<const QUEUE_CAPACITY: usize>(
116 inner: &SchedulerInner<QUEUE_CAPACITY>,
117 worker_id: usize,
118 owner: &mut WorkerQueueOwner<QUEUE_CAPACITY>,
119) -> Option<ScheduledJob> {
120 let worker_count = inner.workers.len();
121 let my_node = inner.worker_numa_nodes.get(worker_id).copied().flatten();
122
123 if let Some(node) = my_node {
137 let start = next_steal_start();
138 for offset in 0..worker_count {
139 let victim_index = (start.wrapping_add(offset)) % worker_count;
140 if victim_index == worker_id {
141 continue;
142 }
143 if inner.worker_numa_nodes.get(victim_index).copied().flatten() != Some(node) {
145 continue;
146 }
147 let victim = &inner.workers[victim_index];
148 if let Some(job) = owner.steal_batch(&victim.queues) {
149 return Some(job);
150 }
151 if let Some(job) = victim.lifo_slot.steal() {
152 return Some(job);
153 }
154 }
155 }
156
157 let start = next_steal_start();
159 for offset in 0..worker_count {
160 let victim_index = (start.wrapping_add(offset)) % worker_count;
161 if victim_index == worker_id {
162 continue;
163 }
164 let victim = &inner.workers[victim_index];
165 if let Some(job) = owner.steal_batch(&victim.queues) {
166 return Some(job);
167 }
168 if let Some(job) = victim.lifo_slot.steal() {
169 return Some(job);
170 }
171 }
172
173 None
174}
175
176fn steal_shared_job<const QUEUE_CAPACITY: usize>(
177 inner: &SchedulerInner<QUEUE_CAPACITY>,
178 worker_id: usize,
179) -> Option<ScheduledJob> {
180 let worker_count = inner.workers.len();
181 let start = next_steal_start();
182 for offset in 0..worker_count {
183 let victim_index = start.wrapping_add(offset) % worker_count;
184 if victim_index == worker_id {
185 continue;
186 }
187 let victim = &inner.workers[victim_index];
188 if let Some(job) = victim.queues.steal_one() {
189 return Some(job);
190 }
191 if let Some(job) = victim.lifo_slot.steal() {
192 return Some(job);
193 }
194 }
195 None
196}
197
198fn next_steal_start() -> usize {
204 use std::cell::Cell;
205 thread_local!(static RNG: Cell<u64> = const { Cell::new(0) });
206 RNG.with(|cell| {
207 let mut x = cell.get();
208 if x == 0 {
209 x = (cell as *const Cell<u64> as u64) | 1;
210 }
211 x ^= x << 13;
212 x ^= x >> 7;
213 x ^= x << 17;
214 cell.set(x);
215 x as usize
216 })
217}
218
219pub(super) fn execute_job<const QUEUE_CAPACITY: usize>(
220 inner: &SchedulerInner<QUEUE_CAPACITY>,
221 worker_id: usize,
222 job: ScheduledJob,
223) {
224 execute_job_with_counters(
225 inner,
226 worker_id,
227 job,
228 &inner.pending_tasks,
229 &inner.active_workers,
230 );
231}
232
233pub(super) fn execute_blocking_job<const QUEUE_CAPACITY: usize>(
234 inner: &SchedulerInner<QUEUE_CAPACITY>,
235 worker_id: usize,
236 job: ScheduledJob,
237) {
238 execute_job_with_counters(
239 inner,
240 worker_id,
241 job,
242 &inner.blocking_pending_tasks,
243 &inner.blocking_active_workers,
244 );
245}
246
247fn execute_job_with_counters<const QUEUE_CAPACITY: usize>(
248 inner: &SchedulerInner<QUEUE_CAPACITY>,
249 worker_id: usize,
250 job: ScheduledJob,
251 pending_tasks: &std::sync::atomic::AtomicUsize,
252 active_workers: &std::sync::atomic::AtomicUsize,
253) {
254 use std::sync::atomic::Ordering;
255 active_workers.fetch_add(1, Ordering::Release);
256 pending_tasks.fetch_sub(1, Ordering::Release);
257
258 if job.execute(worker_id) {
259 inner.completed_tasks.fetch_add(1, Ordering::Relaxed);
260 } else {
261 inner.failed_tasks.fetch_add(1, Ordering::Relaxed);
262 }
263
264 if active_workers.fetch_sub(1, Ordering::SeqCst) == 1 {
275 notify_quiescent(inner);
276 }
277}
278
279fn should_stop<const QUEUE_CAPACITY: usize>(inner: &SchedulerInner<QUEUE_CAPACITY>) -> bool {
280 use std::sync::atomic::Ordering;
281 inner.shutdown.load(Ordering::Acquire) && inner.pending_tasks.load(Ordering::Acquire) == 0
282}
283
284fn spin_for_work<const QUEUE_CAPACITY: usize, const SPIN_LIMIT: usize>(
285 inner: &SchedulerInner<QUEUE_CAPACITY>,
286 worker_id: usize,
287) -> bool {
288 use std::sync::atomic::Ordering;
289 for attempt in 0..SPIN_LIMIT {
290 core::hint::spin_loop();
291 let local = &inner.workers[worker_id];
292 if !local.queues.is_empty()
293 || local.lifo_slot.state.load(Ordering::Relaxed) == 2
294 || should_stop(inner)
295 {
296 return true;
297 }
298
299 if attempt % 32 == 0 && (has_stealable_work(inner, worker_id) || should_stop(inner)) {
301 return true;
302 }
303 }
304
305 false
306}
307
308fn has_stealable_work<const QUEUE_CAPACITY: usize>(
309 inner: &SchedulerInner<QUEUE_CAPACITY>,
310 worker_id: usize,
311) -> bool {
312 use std::sync::atomic::Ordering;
313 let worker_count = inner.workers.len();
314 const STEAL_PROBE_LIMIT: usize = 8;
321 let start = next_steal_start();
322 let probe_count = worker_count.min(STEAL_PROBE_LIMIT);
323 for offset in 0..probe_count {
324 let victim_index = (start.wrapping_add(offset)) % worker_count;
325 if victim_index == worker_id {
326 continue;
327 }
328 let victim = &inner.workers[victim_index];
329 if !victim.queues.is_empty() || victim.lifo_slot.state.load(Ordering::Relaxed) == 2 {
330 return true;
331 }
332 }
333 false
334}
335
336fn wait_for_work<const QUEUE_CAPACITY: usize>(
337 inner: &SchedulerInner<QUEUE_CAPACITY>,
338 worker_id: usize,
339) {
340 use std::sync::atomic::Ordering;
341 inner.idle_workers.set(worker_id);
348 while inner.pending_tasks.load(Ordering::SeqCst) == 0 && !inner.shutdown.load(Ordering::SeqCst)
349 {
350 thread::park();
362 }
363 inner.idle_workers.clear(worker_id);
364}
365
366pub(super) fn wake_worker<const QUEUE_CAPACITY: usize>(worker: &WorkerState<QUEUE_CAPACITY>) {
367 if let Some(thread) = worker.thread.get() {
368 thread.unpark();
369 }
370}
371
372pub(super) fn wake_all_workers<const QUEUE_CAPACITY: usize>(
373 inner: &SchedulerInner<QUEUE_CAPACITY>,
374) {
375 for worker in inner.workers.iter() {
376 wake_worker(worker);
377 }
378}
379
380pub(super) trait ContendedWakable {
381 #[cfg(feature = "scheduler-diagnostics")]
383 fn worker_count(&self) -> usize;
384 #[cfg(feature = "scheduler-diagnostics")]
386 fn wake_worker(&self, worker_index: usize);
387 fn wake_contended<P>(&self, worker_index: usize, previous_pending: usize) -> usize
388 where
389 P: ContendedWakePolicy;
390}
391
392impl<const QUEUE_CAPACITY: usize> ContendedWakable for SchedulerInner<QUEUE_CAPACITY> {
393 #[cfg(feature = "scheduler-diagnostics")]
394 fn worker_count(&self) -> usize {
395 self.workers.len()
396 }
397
398 #[cfg(feature = "scheduler-diagnostics")]
399 fn wake_worker(&self, worker_index: usize) {
400 wake_worker(&self.workers[worker_index]);
401 }
402
403 fn wake_contended<P>(&self, worker_index: usize, previous_pending: usize) -> usize
404 where
405 P: ContendedWakePolicy,
406 {
407 let worker_count = self.workers.len();
408 wake_worker(&self.workers[worker_index]);
409
410 if P::WAKE_LIMIT < 2 || worker_count < 2 {
411 return 1;
412 }
413
414 let peer_index = worker_index.wrapping_add(previous_pending) % worker_count;
415 wake_worker(&self.workers[peer_index]);
416 2
417 }
418}
419
420#[cold]
421#[inline(never)]
422pub(super) fn wake_contended_workers<P>(
423 inner: &impl ContendedWakable,
424 worker_index: usize,
425 previous_pending: usize,
426) -> usize
427where
428 P: ContendedWakePolicy,
429{
430 inner.wake_contended::<P>(worker_index, previous_pending)
431}
432
433#[cfg(feature = "scheduler-diagnostics")]
434#[inline]
435pub(super) fn diagnostic_publish_work_available(
436 inner: &impl ContendedWakable,
437 worker_index: usize,
438 previous_pending: usize,
439) -> usize {
440 let worker_count = inner.worker_count();
441 if previous_pending == 0 {
442 inner.wake_worker(worker_index);
443 1
444 } else if previous_pending < worker_count {
445 wake_contended_workers::<BoundedContendedWake>(inner, worker_index, previous_pending)
446 } else {
447 0
448 }
449}
450
451pub(super) fn notify_quiescent<const QUEUE_CAPACITY: usize>(
452 inner: &SchedulerInner<QUEUE_CAPACITY>,
453) {
454 use std::sync::atomic::Ordering;
455 if inner.join_waiters.load(Ordering::SeqCst) != 0 && is_quiescent(inner) {
460 let _guard = lock_mutex(&inner.wait_lock);
461 if is_quiescent(inner) {
462 inner.wait_signal.notify_all();
463 }
464 }
465}
466
467pub(super) fn is_quiescent<const QUEUE_CAPACITY: usize>(
468 inner: &SchedulerInner<QUEUE_CAPACITY>,
469) -> bool {
470 use std::sync::atomic::Ordering;
471 inner.pending_tasks.load(Ordering::SeqCst) == 0
477 && inner.active_workers.load(Ordering::SeqCst) == 0
478 && inner.blocking_pending_tasks.load(Ordering::SeqCst) == 0
479 && inner.blocking_active_workers.load(Ordering::SeqCst) == 0
480}
481
482pub(super) fn inline_map_reduce<T, Map, Reduce>(
483 count: usize,
484 identity: T,
485 map: Map,
486 reduce: Reduce,
487) -> ExecutorResult<T>
488where
489 Map: Fn(usize) -> T,
490 Reduce: Fn(T, T) -> T,
491{
492 use std::panic::{catch_unwind, AssertUnwindSafe};
493 catch_unwind(AssertUnwindSafe(|| {
494 let mut accumulator = identity;
495 for index in 0..count {
496 accumulator = reduce(accumulator, map(index));
497 }
498 accumulator
499 }))
500 .map_err(|_| ExecutorError::SpawnFailed(moirai_core::error::TaskError::Panicked))
501}
502
503pub(super) fn map_reduce_range<T, Map, Reduce>(
504 start: usize,
505 end: usize,
506 identity: T,
507 map: &Map,
508 reduce: &Reduce,
509) -> T
510where
511 Map: Fn(usize) -> T,
512 Reduce: Fn(T, T) -> T,
513{
514 let mut accumulator = identity;
515 for index in start..end {
516 accumulator = reduce(accumulator, map(index));
517 }
518 accumulator
519}
520
521pub(super) fn indexed_chunk_count(count: usize, worker_count: usize) -> usize {
522 count.min(worker_count.max(1).saturating_add(1))
523}
524
525pub(super) fn indexed_chunk_bounds(
526 count: usize,
527 chunk_count: usize,
528 chunk_index: usize,
529) -> (usize, usize) {
530 let base = count / chunk_count;
531 let remainder = count % chunk_count;
532 let start = chunk_index * base + chunk_index.min(remainder);
533 let len = base + usize::from(chunk_index < remainder);
534 (start, start + len)
535}
536
537pub(super) fn lock_mutex<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
538 mutex
539 .lock()
540 .unwrap_or_else(|poisoned| poisoned.into_inner())
541}
542
543#[cfg(test)]
544mod indexed_chunk_count_tests {
545 use super::{indexed_chunk_bounds, indexed_chunk_count};
546
547 #[test]
548 fn assigns_small_domains_across_available_lanes() {
549 assert_eq!(indexed_chunk_count(9, 8), 9);
550 assert_eq!(indexed_chunk_count(2, 8), 2);
551 assert_eq!(indexed_chunk_count(1, 8), 1);
552 assert_eq!(indexed_chunk_count(0, 8), 0);
553 }
554
555 #[test]
556 fn caps_large_domains_at_workers_plus_caller() {
557 assert_eq!(indexed_chunk_count(1_000_000, 8), 9);
558 }
559
560 #[test]
561 fn single_worker_uses_worker_plus_caller() {
562 assert_eq!(indexed_chunk_count(1024, 1), 2);
563 assert_eq!(indexed_chunk_count(2, 1), 2);
564 }
565
566 #[test]
567 fn balances_remainder_across_every_chunk() {
568 let bounds: Vec<_> = (0..9)
569 .map(|chunk_index| indexed_chunk_bounds(10, 9, chunk_index))
570 .collect();
571 assert_eq!(
572 bounds,
573 vec![
574 (0, 2),
575 (2, 3),
576 (3, 4),
577 (4, 5),
578 (5, 6),
579 (6, 7),
580 (7, 8),
581 (8, 9),
582 (9, 10)
583 ]
584 );
585 }
586}