1use std::cell::UnsafeCell;
15use std::panic::{AssertUnwindSafe, catch_unwind};
16use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
17use std::sync::{Arc, Mutex};
18use std::thread::JoinHandle;
19use std::time::{Duration, Instant};
20
21use serde::{Deserialize, Serialize};
22
23pub fn new_diagnostic_id() -> String {
26 static NEXT: AtomicU64 = AtomicU64::new(1);
27 let id = NEXT
28 .try_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
29 .expect("diagnostic identity exhausted");
30 id.to_string()
31}
32
33#[derive(Default)]
34struct TaskTiming {
35 waits: Vec<(u32, u32, u64, u64)>,
36 start: u64,
37 end: u64,
38 code: Option<i64>,
39}
40
41struct ExecutionTrace {
42 start: Instant,
43 tasks: Vec<Mutex<TaskTiming>>,
45}
46
47impl ExecutionTrace {
48 fn nanos(&self) -> u64 {
49 self.start.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct LaneTaskSpec {
56 pub lane: u32,
58 pub waits: Vec<(u32, u32)>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
64pub enum LaneScheduleError {
65 #[error("task {task} names lane {lane}, but the schedule has {lanes} lanes")]
66 Lane { task: usize, lane: u32, lanes: u32 },
67 #[error("task {task} has an invalid wait on lane {lane} for {completed} tasks")]
68 Wait {
69 task: usize,
70 lane: u32,
71 completed: u32,
72 },
73}
74
75#[derive(Debug, Clone)]
77pub struct LaneSchedule {
78 lanes: u32,
79 tasks: Vec<LaneTaskSpec>,
80 lane_tasks: Vec<Vec<usize>>,
82 diagnostic_id: String,
83}
84
85impl PartialEq for LaneSchedule {
86 fn eq(&self, other: &Self) -> bool {
87 self.lanes == other.lanes
88 && self.tasks == other.tasks
89 && self.lane_tasks == other.lane_tasks
90 }
91}
92impl Eq for LaneSchedule {}
93
94impl LaneSchedule {
95 pub fn new(lanes: u32, tasks: Vec<LaneTaskSpec>) -> Result<Self, LaneScheduleError> {
100 let lanes = lanes.max(1);
101 let mut lane_tasks = vec![Vec::new(); lanes as usize];
102 for (task, spec) in tasks.iter().enumerate() {
103 if spec.lane >= lanes {
104 return Err(LaneScheduleError::Lane {
105 task,
106 lane: spec.lane,
107 lanes,
108 });
109 }
110 for &(lane, completed) in &spec.waits {
111 let listed_before = lane_tasks
112 .get(lane as usize)
113 .is_some_and(|listed: &Vec<usize>| completed as usize <= listed.len());
114 if lane == spec.lane || completed == 0 || !listed_before {
115 return Err(LaneScheduleError::Wait {
116 task,
117 lane,
118 completed,
119 });
120 }
121 }
122 lane_tasks[spec.lane as usize].push(task);
123 }
124 Ok(Self {
125 lanes,
126 tasks,
127 lane_tasks,
128 diagnostic_id: new_diagnostic_id(),
129 })
130 }
131
132 pub fn with_diagnostic_id(mut self, name: String) -> Self {
134 self.diagnostic_id = name;
135 self
136 }
137
138 pub fn lanes(&self) -> u32 {
139 self.lanes
140 }
141
142 pub fn tasks(&self) -> &[LaneTaskSpec] {
143 &self.tasks
144 }
145
146 pub fn run_sequential(&self, runner: &dyn LaneTaskRunner) -> Result<(), LaneTaskFailure> {
148 for task in 0..self.tasks.len() {
149 let code = runner.run_task(task);
150 if code != 0 {
151 return Err(LaneTaskFailure { task, code });
152 }
153 }
154 Ok(())
155 }
156}
157
158pub trait LaneTaskRunner: Sync {
163 fn run_task(&self, task: usize) -> i64;
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub struct LaneTaskFailure {
171 pub task: usize,
172 pub code: i64,
173}
174
175pub const LANE_TASK_PANICKED: i64 = i64::MIN;
177
178#[repr(align(256))]
180struct Padded<T>(T);
181const _: () = assert!(std::mem::align_of::<Padded<u8>>() == celox_state_layout::LANE_SEGMENT_ALIGN);
182
183#[derive(Clone, Copy)]
184struct Job {
185 schedule: *const LaneSchedule,
186 runner: *const (dyn LaneTaskRunner + 'static),
187 trace: *const ExecutionTrace,
188}
189
190const LANE_DONE: u64 = u32::MAX as u64;
192
193const MAX_EPOCH: u64 = u32::MAX as u64;
195
196struct Shared {
197 lanes: usize,
198 job_epoch: Padded<AtomicU64>,
199 job: UnsafeCell<Option<Job>>,
202 progress: Box<[Padded<AtomicU64>]>,
206 abort: Padded<AtomicBool>,
207 failure: Mutex<Option<LaneTaskFailure>>,
208 sleeping: Box<[Padded<AtomicBool>]>,
209 shutdown: AtomicBool,
210 idle_spin: Duration,
211}
212
213unsafe impl Sync for Shared {}
218unsafe impl Send for Shared {}
219
220impl Shared {
221 fn record_failure(&self, failure: LaneTaskFailure) {
222 let mut slot = self
223 .failure
224 .lock()
225 .unwrap_or_else(|poisoned| poisoned.into_inner());
226 if slot.is_none_or(|current| {
227 let priority = |failure: LaneTaskFailure| {
228 (crate::comb_fatal_site(failure.code).is_some(), failure.task)
229 };
230 priority(failure) < priority(current)
231 }) {
232 *slot = Some(failure);
233 }
234 self.abort.0.store(true, Ordering::Release);
235 }
236
237 fn execute_lane(&self, lane: usize, epoch: u64, job: Job) {
238 let (schedule, runner) = unsafe { (&*job.schedule, &*job.runner) };
241 let trace = unsafe { job.trace.as_ref() };
243 let base = epoch << 32;
244 let Some(tasks) = schedule.lane_tasks.get(lane) else {
245 return;
246 };
247 for (sequence, &task) in tasks.iter().enumerate() {
248 let mut timing = trace.map(|_| TaskTiming::default());
249 let mut aborted = self.abort.0.load(Ordering::Acquire);
250 for &(other, completed) in &schedule.tasks[task].waits {
251 if aborted {
252 break;
253 }
254 let target = base | u64::from(completed);
255 let progress = &self.progress[other as usize].0;
256 let mut spins = 0u32;
257 let wait_start = trace.map(ExecutionTrace::nanos);
258 while progress.load(Ordering::Acquire) < target {
259 if self.abort.0.load(Ordering::Acquire) {
260 aborted = true;
261 break;
262 }
263 wait_pause(&mut spins);
264 }
265 if let (Some(trace), Some(timing), Some(start)) = (trace, &mut timing, wait_start) {
266 timing.waits.push((other, completed, start, trace.nanos()));
267 }
268 }
269 if !aborted {
270 if let (Some(trace), Some(timing)) = (trace, &mut timing) {
271 timing.start = trace.nanos();
272 }
273 let code = catch_unwind(AssertUnwindSafe(|| runner.run_task(task)))
274 .unwrap_or(LANE_TASK_PANICKED);
275 if let (Some(trace), Some(timing)) = (trace, &mut timing) {
276 timing.end = trace.nanos();
277 timing.code = Some(code);
278 }
279 if code != 0 {
280 self.record_failure(LaneTaskFailure { task, code });
281 }
282 }
283 if let (Some(trace), Some(timing)) = (trace, timing) {
284 *trace.tasks[task].lock().unwrap_or_else(|e| e.into_inner()) = timing;
285 }
286 self.progress[lane]
287 .0
288 .store(base | (sequence as u64 + 1), Ordering::Release);
289 }
290 }
291}
292
293fn wait_pause(spins: &mut u32) {
296 *spins = spins.wrapping_add(1);
297 if *spins < 2048 {
298 std::hint::spin_loop();
299 } else {
300 std::thread::yield_now();
301 }
302}
303
304pub struct LanePool {
306 shared: Arc<Shared>,
307 workers: Vec<JoinHandle<()>>,
308 epoch: u64,
309 diagnostic_id: String,
310}
311
312impl std::fmt::Debug for LanePool {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 f.debug_struct("LanePool")
315 .field("lanes", &self.shared.lanes)
316 .finish_non_exhaustive()
317 }
318}
319
320impl LanePool {
321 pub const DEFAULT_IDLE_SPIN: Duration = Duration::from_micros(200);
324
325 pub fn new(lanes: u32) -> std::io::Result<Self> {
328 Self::with_idle_spin(lanes, Self::DEFAULT_IDLE_SPIN)
329 }
330
331 pub fn with_idle_spin(lanes: u32, idle_spin: Duration) -> std::io::Result<Self> {
332 let lanes = lanes.max(1) as usize;
333 let shared = Arc::new(Shared {
334 lanes,
335 job_epoch: Padded(AtomicU64::new(0)),
336 job: UnsafeCell::new(None),
337 progress: (0..lanes).map(|_| Padded(AtomicU64::new(0))).collect(),
338 abort: Padded(AtomicBool::new(false)),
339 failure: Mutex::new(None),
340 sleeping: (0..lanes).map(|_| Padded(AtomicBool::new(false))).collect(),
341 shutdown: AtomicBool::new(false),
342 idle_spin,
343 });
344 let mut workers = Vec::with_capacity(lanes - 1);
345 for lane in 1..lanes {
346 let worker_shared = Arc::clone(&shared);
347 let spawned = std::thread::Builder::new()
348 .name(format!("celox-lane-{lane}"))
349 .spawn(move || worker(&worker_shared, lane));
350 match spawned {
351 Ok(handle) => workers.push(handle),
352 Err(error) => {
353 let pool = Self {
354 shared,
355 workers,
356 epoch: 0,
357 diagnostic_id: new_diagnostic_id(),
358 };
359 drop(pool);
360 return Err(error);
361 }
362 }
363 }
364 Ok(Self {
365 shared,
366 workers,
367 epoch: 0,
368 diagnostic_id: new_diagnostic_id(),
369 })
370 }
371
372 pub fn lanes(&self) -> u32 {
373 self.shared.lanes as u32
374 }
375
376 pub fn run(
382 &mut self,
383 schedule: &LaneSchedule,
384 runner: &dyn LaneTaskRunner,
385 ) -> Result<(), LaneTaskFailure> {
386 if schedule.lanes as usize > self.shared.lanes {
387 tracing::trace!(target: "celox::parallel", event = "pool_fallback", reason = "insufficient_workers", requested = schedule.lanes, available = self.shared.lanes);
388 return schedule.run_sequential(runner);
389 }
390 let trace =
391 tracing::enabled!(target: "celox::parallel", tracing::Level::TRACE).then(|| {
392 ExecutionTrace {
393 start: Instant::now(),
394 tasks: (0..schedule.tasks.len())
395 .map(|_| Mutex::new(TaskTiming::default()))
396 .collect(),
397 }
398 });
399 let shared = &*self.shared;
400 if self.epoch >= MAX_EPOCH {
404 for progress in shared.progress.iter() {
405 progress.0.store(0, Ordering::Relaxed);
406 }
407 self.epoch = 0;
408 }
409 self.epoch += 1;
410 let epoch = self.epoch;
411 let runner: &'static dyn LaneTaskRunner = unsafe {
414 std::mem::transmute::<&dyn LaneTaskRunner, &'static dyn LaneTaskRunner>(runner)
415 };
416 let job = Job {
417 schedule: schedule as *const LaneSchedule,
418 runner: runner as *const dyn LaneTaskRunner,
419 trace: trace
420 .as_ref()
421 .map_or(std::ptr::null(), |trace| trace as *const ExecutionTrace),
422 };
423 unsafe {
427 *shared.job.get() = Some(job);
428 }
429 shared.abort.0.store(false, Ordering::Relaxed);
430 shared.job_epoch.0.store(epoch, Ordering::SeqCst);
431 for (index, worker) in self.workers.iter().enumerate() {
432 let sleeping = &shared.sleeping[index + 1].0;
435 if sleeping.load(Ordering::SeqCst) && sleeping.swap(false, Ordering::SeqCst) {
436 worker.thread().unpark();
437 }
438 }
439
440 shared.execute_lane(0, epoch, job);
441
442 let done = (epoch << 32) | LANE_DONE;
443 for lane in 1..shared.lanes {
444 let mut spins = 0u32;
445 while shared.progress[lane].0.load(Ordering::Acquire) != done {
446 wait_pause(&mut spins);
447 }
448 }
449 if let Some(trace) = &trace {
450 let elapsed_ns = trace.nanos();
451 let schedule_id = &schedule.diagnostic_id;
452 let pool = &self.diagnostic_id;
453 tracing::trace!(target: "celox::parallel", event = "dispatch", pool, epoch,
454 schedule = schedule_id, lanes = schedule.lanes, elapsed_ns);
455 for (task, timing) in trace.tasks.iter().enumerate() {
457 let timing = timing.lock().unwrap_or_else(|e| e.into_inner());
458 tracing::trace!(target: "celox::parallel", event = "task_run", pool, epoch,
459 schedule = schedule_id, task, lane = schedule.tasks[task].lane,
460 start_ns = timing.start, end_ns = timing.end, skipped = timing.code.is_none(), code = timing.code.unwrap_or(0));
461 for &(other, completed, start_ns, end_ns) in &timing.waits {
462 tracing::trace!(target: "celox::parallel", event = "task_wait", pool, epoch,
463 task, lane = schedule.tasks[task].lane, other, completed, start_ns, end_ns);
464 }
465 }
466 }
467 unsafe {
469 *shared.job.get() = None;
470 }
471 let failure = shared
472 .failure
473 .lock()
474 .unwrap_or_else(|poisoned| poisoned.into_inner())
475 .take();
476 match failure {
477 Some(failure) => Err(failure),
478 None => Ok(()),
479 }
480 }
481}
482
483fn worker(shared: &Shared, lane: usize) {
484 let mut seen = 0u64;
485 loop {
486 let mut idle_since = None::<Instant>;
487 let mut spins = 0u32;
488 let epoch = loop {
489 let epoch = shared.job_epoch.0.load(Ordering::Acquire);
490 if epoch != seen || shared.shutdown.load(Ordering::Acquire) {
491 break epoch;
492 }
493 spins = spins.wrapping_add(1);
494 std::hint::spin_loop();
495 if !spins.is_multiple_of(256) {
496 continue;
497 }
498 let since = *idle_since.get_or_insert_with(Instant::now);
499 if since.elapsed() < shared.idle_spin {
500 continue;
501 }
502 shared.sleeping[lane].0.store(true, Ordering::SeqCst);
505 let epoch = shared.job_epoch.0.load(Ordering::SeqCst);
506 if epoch != seen || shared.shutdown.load(Ordering::SeqCst) {
507 shared.sleeping[lane].0.store(false, Ordering::SeqCst);
508 break epoch;
509 }
510 std::thread::park();
511 shared.sleeping[lane].0.store(false, Ordering::SeqCst);
512 idle_since = None;
513 };
514 if shared.shutdown.load(Ordering::Acquire) {
515 return;
516 }
517 seen = epoch;
518 let job = unsafe { *shared.job.get() };
520 if let Some(job) = job {
521 shared.execute_lane(lane, epoch, job);
522 }
523 shared.progress[lane]
525 .0
526 .store((epoch << 32) | LANE_DONE, Ordering::Release);
527 }
528}
529
530impl Drop for LanePool {
531 fn drop(&mut self) {
532 self.shared.shutdown.store(true, Ordering::SeqCst);
533 self.shared.job_epoch.0.fetch_add(1, Ordering::SeqCst);
534 for worker in &self.workers {
535 worker.thread().unpark();
536 }
537 for worker in self.workers.drain(..) {
538 let _ = worker.join();
539 }
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use std::sync::atomic::AtomicI64;
547
548 struct Recorder {
551 log: Mutex<Vec<usize>>,
552 done: Vec<AtomicBool>,
553 predecessors: Vec<Vec<usize>>,
554 fail: Option<usize>,
555 calls: AtomicI64,
556 }
557
558 impl LaneTaskRunner for Recorder {
559 fn run_task(&self, task: usize) -> i64 {
560 self.calls.fetch_add(1, Ordering::Relaxed);
561 for &predecessor in &self.predecessors[task] {
562 assert!(
563 self.done[predecessor].load(Ordering::Acquire),
564 "task {task} ran before {predecessor}"
565 );
566 }
567 self.log.lock().unwrap().push(task);
568 self.done[task].store(true, Ordering::Release);
569 if self.fail == Some(task) { 7 } else { 0 }
570 }
571 }
572
573 fn chain_schedule() -> (LaneSchedule, Vec<Vec<usize>>) {
574 let tasks = vec![
576 LaneTaskSpec {
577 lane: 0,
578 waits: vec![],
579 },
580 LaneTaskSpec {
581 lane: 1,
582 waits: vec![(0, 1)],
583 },
584 LaneTaskSpec {
585 lane: 0,
586 waits: vec![],
587 },
588 LaneTaskSpec {
589 lane: 2,
590 waits: vec![(1, 1), (0, 2)],
591 },
592 ];
593 let predecessors = vec![vec![], vec![0], vec![0], vec![1, 2]];
594 (LaneSchedule::new(3, tasks).unwrap(), predecessors)
595 }
596
597 fn recorder(predecessors: Vec<Vec<usize>>, fail: Option<usize>) -> Recorder {
598 Recorder {
599 log: Mutex::new(Vec::new()),
600 done: (0..predecessors.len())
601 .map(|_| AtomicBool::new(false))
602 .collect(),
603 predecessors,
604 fail,
605 calls: AtomicI64::new(0),
606 }
607 }
608
609 #[test]
610 fn schedules_reject_forward_or_self_waits() {
611 let forward = vec![LaneTaskSpec {
612 lane: 0,
613 waits: vec![(1, 1)],
614 }];
615 assert!(LaneSchedule::new(2, forward).is_err());
616 let self_wait = vec![
617 LaneTaskSpec {
618 lane: 0,
619 waits: vec![],
620 },
621 LaneTaskSpec {
622 lane: 0,
623 waits: vec![(0, 1)],
624 },
625 ];
626 assert!(LaneSchedule::new(1, self_wait).is_err());
627 }
628
629 #[test]
630 fn repeated_executions_honour_waits() {
631 let (schedule, predecessors) = chain_schedule();
632 let mut pool = LanePool::with_idle_spin(3, Duration::from_micros(0)).unwrap();
633 for _ in 0..2000 {
634 let runner = recorder(predecessors.clone(), None);
635 pool.run(&schedule, &runner).unwrap();
636 assert_eq!(runner.log.lock().unwrap().len(), 4);
637 }
638 }
639
640 #[test]
641 fn failures_skip_pending_work_and_report_the_first_task() {
642 let (schedule, predecessors) = chain_schedule();
643 let mut pool = LanePool::new(3).unwrap();
644 let runner = recorder(predecessors.clone(), Some(0));
645 assert_eq!(
646 pool.run(&schedule, &runner),
647 Err(LaneTaskFailure { task: 0, code: 7 })
648 );
649 assert!(!runner.done[1].load(Ordering::Acquire));
651 assert!(!runner.done[3].load(Ordering::Acquire));
652 let runner = recorder(predecessors, None);
654 pool.run(&schedule, &runner).unwrap();
655 assert_eq!(runner.calls.load(Ordering::Relaxed), 4);
656 }
657
658 #[test]
659 fn concurrent_loop_and_internal_failures_take_precedence_over_fatal_captures() {
660 struct Runner {
661 started: std::sync::Barrier,
662 codes: [i64; 2],
663 }
664 impl LaneTaskRunner for Runner {
665 fn run_task(&self, task: usize) -> i64 {
666 self.started.wait();
667 self.codes[task]
668 }
669 }
670 let schedule = LaneSchedule::new(
671 2,
672 vec![
673 LaneTaskSpec {
674 lane: 0,
675 waits: vec![],
676 },
677 LaneTaskSpec {
678 lane: 1,
679 waits: vec![],
680 },
681 ],
682 )
683 .unwrap();
684 let mut pool = LanePool::new(2).unwrap();
685 for code in [1, 2000, LANE_TASK_PANICKED] {
686 for task in [0, 1] {
687 let mut codes = [crate::comb_fatal_code(1); 2];
688 codes[task] = code;
689 let runner = Runner {
690 started: std::sync::Barrier::new(2),
691 codes,
692 };
693 assert_eq!(
694 pool.run(&schedule, &runner),
695 Err(LaneTaskFailure { task, code })
696 );
697 }
698 }
699 let runner = Runner {
700 started: std::sync::Barrier::new(2),
701 codes: [crate::comb_fatal_code(2), crate::comb_fatal_code(1)],
702 };
703 assert_eq!(
704 pool.run(&schedule, &runner),
705 Err(LaneTaskFailure {
706 task: 0,
707 code: crate::comb_fatal_code(2),
708 })
709 );
710 }
711
712 #[test]
713 fn epochs_restart_without_releasing_waits_early() {
714 let (schedule, predecessors) = chain_schedule();
715 let mut pool = LanePool::new(3).unwrap();
716 pool.epoch = MAX_EPOCH - 2;
717 for _ in 0..4 {
718 let runner = recorder(predecessors.clone(), None);
719 pool.run(&schedule, &runner).unwrap();
720 assert_eq!(runner.calls.load(Ordering::Relaxed), 4);
721 }
722 assert!(pool.epoch < 4);
723 }
724
725 #[test]
726 fn sequential_fallback_preserves_list_order() {
727 let (schedule, predecessors) = chain_schedule();
728 let runner = recorder(predecessors, None);
729 schedule.run_sequential(&runner).unwrap();
730 assert_eq!(*runner.log.lock().unwrap(), vec![0, 1, 2, 3]);
731 }
732
733 #[test]
734 fn diagnostic_ids_distinguish_equal_topologies_and_recreated_pools() {
735 let (first, _) = chain_schedule();
736 let (second, _) = chain_schedule();
737 assert_ne!(first.diagnostic_id, second.diagnostic_id);
738 assert_eq!(first, second);
739 assert_eq!(first, second.clone().with_diagnostic_id("different".into()));
740 let different = LaneSchedule::new(
741 1,
742 vec![LaneTaskSpec {
743 lane: 0,
744 waits: vec![],
745 }],
746 )
747 .unwrap();
748 assert_ne!(first, different);
749 assert_eq!(first.clone().diagnostic_id, first.diagnostic_id);
750 let first_pool = LanePool::new(1).unwrap();
751 let id = first_pool.diagnostic_id.clone();
752 drop(first_pool);
753 let second_pool = LanePool::new(1).unwrap();
754 assert_ne!(id, second_pool.diagnostic_id);
755 }
756
757 #[test]
758 fn traced_failure_and_recovery_record_skips_and_real_waits() {
759 use std::collections::BTreeMap;
760 #[derive(Clone)]
761 struct Capture(Arc<Mutex<Vec<BTreeMap<String, String>>>>);
762 struct Fields(BTreeMap<String, String>);
763 impl tracing::field::Visit for Fields {
764 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
765 self.0.insert(field.name().into(), format!("{value:?}"));
766 }
767 }
768 impl tracing::Subscriber for Capture {
769 fn max_level_hint(&self) -> Option<tracing::metadata::LevelFilter> {
770 Some(tracing::metadata::LevelFilter::TRACE)
771 }
772 fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
773 metadata.target() == "celox::parallel"
774 }
775 fn register_callsite(
776 &self,
777 _: &'static tracing::Metadata<'static>,
778 ) -> tracing::subscriber::Interest {
779 tracing::subscriber::Interest::sometimes()
780 }
781 fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
782 tracing::span::Id::from_u64(1)
783 }
784 fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
785 fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
786 fn enter(&self, _: &tracing::span::Id) {}
787 fn exit(&self, _: &tracing::span::Id) {}
788 fn event(&self, event: &tracing::Event<'_>) {
789 let mut fields = Fields(BTreeMap::new());
790 event.record(&mut fields);
791 self.0.lock().unwrap().push(fields.0);
792 }
793 }
794 let events = Arc::new(Mutex::new(Vec::new()));
795 tracing::subscriber::with_default(Capture(events.clone()), || {
796 let (schedule, predecessors) = chain_schedule();
797 let mut pool = LanePool::new(3).unwrap();
798 assert!(
799 pool.run(&schedule, &recorder(predecessors.clone(), Some(0)))
800 .is_err()
801 );
802 pool.run(&schedule, &recorder(predecessors, None)).unwrap();
803 });
804 let events = events.lock().unwrap();
805 let rows = |name: &str| {
806 let name = format!("{name:?}");
807 events.iter().filter(move |e| e["event"] == name)
808 };
809 assert_eq!(rows("dispatch").count(), 2, "{events:?}");
810 let runs = rows("task_run").collect::<Vec<_>>();
811 assert_eq!(runs.len(), 8);
812 assert!(runs.iter().any(|e| e["skipped"] == "true"));
813 for event in runs.iter().filter(|e| e["epoch"] == "2") {
814 assert_eq!(event["skipped"], "false");
815 assert_eq!(event["code"], "0");
816 assert!(
817 event["end_ns"].parse::<u64>().unwrap()
818 >= event["start_ns"].parse::<u64>().unwrap()
819 );
820 }
821 assert_eq!(rows("task_wait").filter(|e| e["epoch"] == "2").count(), 3);
822 }
823}
824
825#[derive(Debug, Clone, Copy, PartialEq, Eq)]
827pub enum KernelChoice {
828 Parallel,
829 Sequential,
830}
831
832#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
834pub enum KernelSelection {
835 #[default]
838 Measured,
839 Parallel,
841 Sequential,
843}
844
845#[derive(Debug)]
860pub struct KernelSelector {
861 diagnostic_id: String,
862 state: SelectorState,
863 recheck_interval: u64,
866}
867
868#[derive(Debug, Clone)]
869enum SelectorState {
870 Calibrating {
871 round: u32,
872 position: u32,
873 samples: [Vec<u64>; 2],
874 },
875 Decided {
876 choice: KernelChoice,
877 remaining: u64,
879 },
880}
881
882impl Clone for KernelSelector {
883 fn clone(&self) -> Self {
884 Self {
885 diagnostic_id: new_diagnostic_id(),
886 state: self.state.clone(),
887 recheck_interval: self.recheck_interval,
888 }
889 }
890}
891
892impl KernelSelector {
893 const BLOCK: u32 = 16;
895 const DISCARDED: u32 = 4;
897 const ROUNDS: u32 = 3;
899 const PARALLEL_MARGIN_PERCENT: u64 = 95;
902 const FIRST_RECHECK: u64 = 1 << 16;
904 const MAX_RECHECK: u64 = 1 << 26;
906
907 pub fn new(selection: KernelSelection) -> Self {
908 let decided = |choice| SelectorState::Decided {
909 choice,
910 remaining: u64::MAX,
911 };
912 match selection {
913 KernelSelection::Measured => Self {
914 diagnostic_id: new_diagnostic_id(),
915 state: Self::calibrating(),
916 recheck_interval: Self::FIRST_RECHECK,
917 },
918 KernelSelection::Parallel => Self {
919 diagnostic_id: new_diagnostic_id(),
920 state: decided(KernelChoice::Parallel),
921 recheck_interval: 0,
922 },
923 KernelSelection::Sequential => Self {
924 diagnostic_id: new_diagnostic_id(),
925 state: decided(KernelChoice::Sequential),
926 recheck_interval: 0,
927 },
928 }
929 }
930
931 pub fn diagnostic_id(&self) -> &str {
933 &self.diagnostic_id
934 }
935
936 fn calibrating() -> SelectorState {
937 SelectorState::Calibrating {
938 round: 0,
939 position: 0,
940 samples: [Vec::new(), Vec::new()],
941 }
942 }
943
944 pub fn next(&self) -> (KernelChoice, bool) {
947 match &self.state {
948 SelectorState::Decided { choice, .. } => (*choice, false),
949 SelectorState::Calibrating { round, .. } => {
950 let choice = if round % 2 == 0 {
952 KernelChoice::Parallel
953 } else {
954 KernelChoice::Sequential
955 };
956 (choice, true)
957 }
958 }
959 }
960
961 pub fn decided(&self) -> Option<KernelChoice> {
963 match self.state {
964 SelectorState::Decided { choice, .. } => Some(choice),
965 SelectorState::Calibrating { .. } => None,
966 }
967 }
968
969 pub fn advance(&mut self, calls: u64) {
972 if self.recheck_interval == 0 {
973 return;
974 }
975 if let SelectorState::Decided { remaining, .. } = &mut self.state {
976 *remaining = remaining.saturating_sub(calls);
977 if *remaining == 0 {
978 self.state = Self::calibrating();
979 }
980 }
981 }
982
983 pub fn record(&mut self, choice: KernelChoice, nanos: u64) {
986 let SelectorState::Calibrating {
987 round,
988 position,
989 samples,
990 } = &mut self.state
991 else {
992 return;
993 };
994 if *position >= Self::DISCARDED {
995 samples[usize::from(choice == KernelChoice::Sequential)].push(nanos);
996 }
997 *position += 1;
998 if *position < Self::BLOCK {
999 return;
1000 }
1001 *position = 0;
1002 *round += 1;
1003 if *round < 2 * Self::ROUNDS {
1004 return;
1005 }
1006 let median = |values: &mut Vec<u64>| {
1007 values.sort_unstable();
1008 values.get(values.len() / 2).copied().unwrap_or(u64::MAX)
1009 };
1010 let [parallel, sequential] = samples;
1011 let parallel = median(parallel);
1012 let sequential = median(sequential);
1013 let choice = if parallel.saturating_mul(100)
1014 < sequential.saturating_mul(Self::PARALLEL_MARGIN_PERCENT)
1015 {
1016 KernelChoice::Parallel
1017 } else {
1018 KernelChoice::Sequential
1019 };
1020 tracing::debug!(target: "celox::parallel::selection", event = "selector_decision",
1021 selector = self.diagnostic_id.as_str(), parallel_ns = parallel, sequential_ns = sequential, choice = ?choice);
1022 tracing::debug!(
1023 "[parallel] kernel selection: parallel median {parallel} ns, sequential median {sequential} ns -> {choice:?}"
1024 );
1025 self.state = SelectorState::Decided {
1026 choice,
1027 remaining: self.recheck_interval,
1028 };
1029 self.recheck_interval = self
1030 .recheck_interval
1031 .saturating_mul(2)
1032 .min(Self::MAX_RECHECK);
1033 }
1034}
1035
1036#[cfg(test)]
1037mod selector_tests {
1038 use super::*;
1039
1040 fn calibrate(parallel: u64, sequential: u64) -> KernelChoice {
1041 let mut selector = KernelSelector::new(KernelSelection::Measured);
1042 while selector.decided().is_none() {
1043 let (choice, timed) = selector.next();
1044 assert!(timed);
1045 selector.record(
1046 choice,
1047 match choice {
1048 KernelChoice::Parallel => parallel,
1049 KernelChoice::Sequential => sequential,
1050 },
1051 );
1052 }
1053 selector.decided().unwrap()
1054 }
1055
1056 #[test]
1057 fn selector_identity_survives_moves_without_reuse() {
1058 let selector = KernelSelector::new(KernelSelection::Measured);
1059 let id = selector.diagnostic_id().to_owned();
1060 let mut moved = Box::new(selector);
1061 assert_eq!(moved.diagnostic_id(), id);
1062 let cloned = moved.clone();
1063 assert_ne!(cloned.diagnostic_id(), id);
1064 assert_eq!(cloned.next(), moved.next());
1065 while moved.decided().is_none() {
1066 let (choice, _) = moved.next();
1067 moved.record(choice, 100);
1068 }
1069 assert_eq!(moved.diagnostic_id(), id);
1070 let decided_clone = moved.clone();
1071 assert_ne!(decided_clone.diagnostic_id(), id);
1072 assert_eq!(decided_clone.next(), moved.next());
1073 drop(moved);
1074 let replacement = KernelSelector::new(KernelSelection::Measured);
1075 assert_ne!(replacement.diagnostic_id(), id);
1076 assert_ne!(replacement.diagnostic_id(), cloned.diagnostic_id());
1077 }
1078
1079 #[test]
1080 fn measured_selection_keeps_the_faster_kernel() {
1081 assert_eq!(calibrate(500, 1000), KernelChoice::Parallel);
1082 assert_eq!(calibrate(1000, 500), KernelChoice::Sequential);
1083 assert_eq!(calibrate(980, 1000), KernelChoice::Sequential);
1085 }
1086
1087 #[test]
1088 fn measured_decisions_are_checked_again_with_growing_intervals() {
1089 let mut selector = KernelSelector::new(KernelSelection::Measured);
1090 let mut calibrations = 0;
1091 let mut checks = Vec::new();
1092 let mut calls = 0u64;
1093 while checks.len() < 3 {
1094 let (choice, timed) = selector.next();
1095 if timed {
1096 let was_measuring = selector.decided().is_none();
1097 selector.record(
1098 choice,
1099 if choice == KernelChoice::Parallel {
1100 1
1101 } else {
1102 2
1103 },
1104 );
1105 if was_measuring && selector.decided().is_some() {
1106 calibrations += 1;
1107 checks.push(calls);
1108 }
1109 } else {
1110 selector.advance(1 << 12);
1111 }
1112 calls += 1;
1113 }
1114 assert_eq!(calibrations, 3);
1115 let first = checks[1] - checks[0];
1117 let second = checks[2] - checks[1];
1118 assert!(second > first);
1119 assert_eq!(selector.decided(), Some(KernelChoice::Parallel));
1120 }
1121
1122 #[test]
1123 fn fixed_selections_do_not_measure() {
1124 for (selection, expected) in [
1125 (KernelSelection::Parallel, KernelChoice::Parallel),
1126 (KernelSelection::Sequential, KernelChoice::Sequential),
1127 ] {
1128 let mut selector = KernelSelector::new(selection);
1129 assert_eq!(selector.next(), (expected, false));
1130 selector.advance(u64::MAX);
1131 assert_eq!(selector.next(), (expected, false));
1132 }
1133 }
1134}