use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::OnceLock;
use web_time::Instant;
use rayon::prelude::*;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Stage {
Producer,
#[allow(dead_code)]
Present,
}
const MIN_POOLED_ITEMS: usize = 256;
const PRODUCER_CHUNK_FACTOR: usize = 4;
#[derive(Default)]
struct TelemetryCounters {
submissions: AtomicU64,
contended_submissions: AtomicU64,
queue_delay_ns: AtomicU64,
exec_ns: AtomicU64,
}
#[allow(dead_code)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct ExecutorTelemetry {
pub submissions: u64,
pub contended_submissions: u64,
pub queue_delay_ns: u64,
pub exec_ns: u64,
}
pub(crate) struct StageExecutor {
pool: rayon::ThreadPool,
lanes: usize,
active_submissions: AtomicUsize,
telemetry: TelemetryCounters,
}
impl StageExecutor {
pub(crate) fn new(lanes: usize) -> Self {
let lanes = lanes.max(1);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(lanes)
.thread_name(|index| format!("cranpose-exec-{index}"))
.build()
.expect("build stage executor pool");
Self {
pool,
lanes,
active_submissions: AtomicUsize::new(0),
telemetry: TelemetryCounters::default(),
}
}
#[allow(dead_code)]
pub(crate) fn lanes(&self) -> usize {
self.lanes
}
#[allow(dead_code)]
pub(crate) fn telemetry(&self) -> ExecutorTelemetry {
ExecutorTelemetry {
submissions: self.telemetry.submissions.load(Ordering::Relaxed),
contended_submissions: self.telemetry.contended_submissions.load(Ordering::Relaxed),
queue_delay_ns: self.telemetry.queue_delay_ns.load(Ordering::Relaxed),
exec_ns: self.telemetry.exec_ns.load(Ordering::Relaxed),
}
}
fn chunk_len(&self, stage: Stage, len: usize) -> usize {
let chunks = match stage {
Stage::Producer => self.lanes * PRODUCER_CHUNK_FACTOR,
Stage::Present => self.lanes,
};
len.div_ceil(chunks).max(1)
}
fn run_pooled<R, B>(&self, body: B) -> R
where
R: Send,
B: FnOnce(&(dyn Fn() + Sync)) -> R + Send,
{
let previously_active = self.active_submissions.fetch_add(1, Ordering::Relaxed);
self.telemetry.submissions.fetch_add(1, Ordering::Relaxed);
if previously_active > 0 {
self.telemetry
.contended_submissions
.fetch_add(1, Ordering::Relaxed);
}
let submitted = Instant::now();
let first_chunk_delay_ns = AtomicU64::new(u64::MAX);
let mark_first_chunk = || {
if first_chunk_delay_ns.load(Ordering::Relaxed) != u64::MAX {
return;
}
let delay = submitted.elapsed().as_nanos().min(u64::MAX as u128) as u64;
let _ = first_chunk_delay_ns.compare_exchange(
u64::MAX,
delay,
Ordering::Relaxed,
Ordering::Relaxed,
);
};
let result = self.pool.install(|| body(&mark_first_chunk));
let exec = submitted.elapsed().as_nanos().min(u64::MAX as u128) as u64;
self.telemetry.exec_ns.fetch_add(exec, Ordering::Relaxed);
let delay = first_chunk_delay_ns.load(Ordering::Relaxed);
if delay != u64::MAX {
self.telemetry
.queue_delay_ns
.fetch_add(delay, Ordering::Relaxed);
}
self.active_submissions.fetch_sub(1, Ordering::Relaxed);
result
}
pub(crate) fn map_fill<I, O, F>(&self, stage: Stage, input: &[I], out: &mut Vec<O>, f: F)
where
I: Sync,
O: Send,
F: Fn(&I) -> O + Send + Sync,
{
let len = input.len();
if self.lanes == 1 || len < MIN_POOLED_ITEMS {
out.clear();
out.reserve(len);
out.extend(input.iter().map(f));
return;
}
let chunk = self.chunk_len(stage, len);
self.run_pooled(|mark_first_chunk| {
input
.par_iter()
.with_min_len(chunk)
.map(|item| {
mark_first_chunk();
f(item)
})
.collect_into_vec(out);
});
}
}
impl cranpose_ui_graphics::VerifyExecutor for StageExecutor {
fn for_each(&self, jobs: usize, run: &(dyn Fn(usize) + Sync)) {
if self.lanes == 1 || jobs == 0 {
for job in 0..jobs {
run(job);
}
return;
}
self.run_pooled(|mark_first_chunk| {
(0..jobs).into_par_iter().for_each(|job| {
mark_first_chunk();
run(job);
});
});
}
}
pub(crate) fn stage_executor() -> &'static StageExecutor {
static EXECUTOR: OnceLock<StageExecutor> = OnceLock::new();
EXECUTOR.get_or_init(|| StageExecutor::new(crate::render::shape_convert_worker_count().max(1)))
}
#[cfg(test)]
mod tests {
use super::*;
use cranpose_ui_graphics::VerifyExecutor;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn small_inputs_stay_serial_but_correct() {
let executor = StageExecutor::new(4);
let input: Vec<u32> = (0..10).collect();
let mut output: Vec<u32> = Vec::new();
executor.map_fill(Stage::Present, &input, &mut output, |value| value + 7);
assert_eq!(output, (7..17).collect::<Vec<_>>());
assert_eq!(executor.telemetry().submissions, 0);
}
#[test]
fn map_fill_matches_serial_and_reuses_capacity() {
let executor = StageExecutor::new(4);
let input: Vec<u64> = (0..10_000).collect();
let mut out: Vec<String> = Vec::new();
executor.map_fill(Stage::Producer, &input, &mut out, |value| {
format!("{value}")
});
assert_eq!(out.len(), input.len());
assert!(out.iter().enumerate().all(|(i, v)| *v == format!("{i}")));
let capacity = out.capacity();
executor.map_fill(Stage::Producer, &input[..500], &mut out, |value| {
format!("{value}")
});
assert_eq!(out.len(), 500);
assert!(
out.capacity() >= capacity.min(input.len()),
"refill must not shed capacity"
);
assert!(out.iter().enumerate().all(|(i, v)| *v == format!("{i}")));
}
#[test]
fn verify_executor_runs_every_job_exactly_once() {
let executor = StageExecutor::new(3);
let hits: Vec<AtomicUsize> = (0..97).map(|_| AtomicUsize::new(0)).collect();
for _ in 0..50 {
executor.for_each(hits.len(), &|job| {
hits[job].fetch_add(1, Ordering::Relaxed);
});
}
assert!(hits.iter().all(|hit| hit.load(Ordering::Relaxed) == 50));
}
#[test]
fn simultaneous_submissions_from_two_threads_stay_correct() {
let executor = StageExecutor::new(4);
let input: Vec<u64> = (0..20_000).collect();
std::thread::scope(|scope| {
let producer = scope.spawn(|| {
let mut out: Vec<u64> = Vec::new();
for round in 0..100u64 {
executor.map_fill(Stage::Producer, &input, &mut out, |v| v * 2 + round);
assert!(out
.iter()
.enumerate()
.all(|(i, &v)| v == i as u64 * 2 + round));
}
});
let present = scope.spawn(|| {
let mut out: Vec<u64> = Vec::new();
for round in 0..100u64 {
executor.map_fill(Stage::Present, &input, &mut out, |v| v * 5 + round);
assert!(out
.iter()
.enumerate()
.all(|(i, &v)| v == i as u64 * 5 + round));
}
});
producer.join().expect("producer thread");
present.join().expect("present thread");
});
let telemetry = executor.telemetry();
assert!(telemetry.submissions >= 200);
assert!(
telemetry.contended_submissions > 0,
"two hammering threads must overlap at least once"
);
}
#[test]
fn panicking_job_propagates_and_the_executor_survives() {
let executor = StageExecutor::new(4);
let input: Vec<u32> = (0..5_000).collect();
let mut output: Vec<u32> = Vec::new();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
executor.map_fill(Stage::Producer, &input, &mut output, |value| {
assert!(*value != 2_500, "poisoned item");
*value
});
}));
assert!(result.is_err(), "the panic must reach the submitter");
executor.map_fill(Stage::Producer, &input, &mut output, |value| value + 1);
assert!(output.iter().enumerate().all(|(i, &v)| v == i as u32 + 1));
}
#[test]
fn teardown_joins_cleanly_after_use() {
let executor = StageExecutor::new(4);
let input: Vec<u64> = (0..10_000).collect();
let mut out = Vec::new();
executor.map_fill(Stage::Producer, &input, &mut out, |v| v + 1);
assert_eq!(out.len(), input.len());
drop(executor); }
#[test]
fn nested_submissions_do_not_deadlock() {
let executor = StageExecutor::new(2);
let outer: Vec<u32> = (0..600).collect();
let mut out: Vec<u32> = Vec::new();
executor.map_fill(Stage::Producer, &outer, &mut out, |v| v * 2);
assert!(out.iter().enumerate().all(|(i, &v)| v == i as u32 * 2));
}
#[test]
fn telemetry_records_queue_delay_and_exec_time() {
let executor = StageExecutor::new(2);
let input: Vec<u64> = (0..10_000).collect();
let mut out = Vec::new();
executor.map_fill(Stage::Producer, &input, &mut out, |v| v + 1);
let telemetry = executor.telemetry();
assert_eq!(telemetry.submissions, 1);
assert!(telemetry.exec_ns > 0);
assert!(telemetry.queue_delay_ns < telemetry.exec_ns);
}
}