laddu-core 0.19.0

Core of the laddu library
Documentation
#[cfg(feature = "rayon")]
use std::sync::Arc;
use std::sync::{Mutex, MutexGuard};

use super::ThreadPoolManager;
#[cfg(feature = "rayon")]
use crate::execution::thread_pool::ThreadExecutor;

static GLOBAL_THREAD_COUNT_TEST_GUARD: Mutex<()> = Mutex::new(());

struct GlobalThreadCountReset {
    previous: Option<usize>,
    _guard: MutexGuard<'static, ()>,
}

impl GlobalThreadCountReset {
    fn new(n_threads: usize) -> Self {
        let guard = GLOBAL_THREAD_COUNT_TEST_GUARD
            .lock()
            .expect("global thread-count test guard should not be poisoned");
        let previous = ThreadPoolManager::global_thread_count();
        ThreadPoolManager::set_global_thread_count(n_threads);
        Self {
            previous,
            _guard: guard,
        }
    }
}

impl Drop for GlobalThreadCountReset {
    fn drop(&mut self) {
        ThreadPoolManager::set_global_thread_count(self.previous.unwrap_or(0));
    }
}

#[cfg(feature = "rayon")]
#[test]
fn thread_pool_manager_reuses_cached_pool_for_same_thread_count() {
    let manager = ThreadPoolManager::default();
    let first_pool = match manager
        .executor_for_threads(2)
        .expect("pool for two threads should build")
    {
        ThreadExecutor::Dedicated(pool) => pool,
        ThreadExecutor::Ambient => panic!("executor should be dedicated"),
    };
    let second_pool = match manager
        .executor_for_threads(2)
        .expect("pool for two threads should be cached")
    {
        ThreadExecutor::Dedicated(pool) => pool,
        ThreadExecutor::Ambient => panic!("executor should be dedicated"),
    };
    assert!(Arc::ptr_eq(&first_pool, &second_pool));
}

#[cfg(feature = "rayon")]
#[test]
fn thread_pool_manager_separates_distinct_thread_counts() {
    let manager = ThreadPoolManager::default();
    let two_thread_pool = match manager
        .executor_for_threads(2)
        .expect("pool for two threads should build")
    {
        ThreadExecutor::Dedicated(pool) => pool,
        ThreadExecutor::Ambient => panic!("executor should be dedicated"),
    };
    let three_thread_pool = match manager
        .executor_for_threads(3)
        .expect("pool for three threads should build")
    {
        ThreadExecutor::Dedicated(pool) => pool,
        ThreadExecutor::Ambient => panic!("executor should be dedicated"),
    };
    assert!(!Arc::ptr_eq(&two_thread_pool, &three_thread_pool));
}

#[cfg(feature = "rayon")]
#[test]
fn thread_pool_manager_replaces_cached_pool_when_thread_count_changes() {
    let manager = ThreadPoolManager::default();
    let first_two_thread_pool = match manager
        .executor_for_threads(2)
        .expect("pool for two threads should build")
    {
        ThreadExecutor::Dedicated(pool) => pool,
        ThreadExecutor::Ambient => panic!("executor should be dedicated"),
    };
    manager
        .executor_for_threads(3)
        .expect("pool for three threads should replace the cache");
    let second_two_thread_pool = match manager
        .executor_for_threads(2)
        .expect("pool for two threads should rebuild after cache replacement")
    {
        ThreadExecutor::Dedicated(pool) => pool,
        ThreadExecutor::Ambient => panic!("executor should be dedicated"),
    };
    assert!(!Arc::ptr_eq(
        &first_two_thread_pool,
        &second_two_thread_pool
    ));
}

#[test]
fn thread_pool_manager_treats_zero_threads_as_global_fallback() {
    let _reset = GlobalThreadCountReset::new(0);
    let manager = ThreadPoolManager::default();
    let value = manager
        .install(Some(0), || 17usize)
        .expect("global fallback install should succeed");
    assert_eq!(value, 17);
    #[cfg(feature = "rayon")]
    assert!(manager.dedicated_pool.read().is_none());
}

#[cfg(feature = "rayon")]
#[test]
fn thread_pool_manager_reuses_cached_pool_across_many_short_installs() {
    let manager = ThreadPoolManager::default();
    let first_pool = match manager
        .executor_for_threads(2)
        .expect("pool for two threads should build")
    {
        ThreadExecutor::Dedicated(pool) => pool,
        ThreadExecutor::Ambient => panic!("executor should be dedicated"),
    };

    let total = (0usize..128)
        .map(|index| {
            let value = manager
                .install(Some(2), || index + 1)
                .expect("repeated short install should succeed");
            let cached_pool = match manager
                .executor_for_threads(2)
                .expect("pool for two threads should remain cached")
            {
                ThreadExecutor::Dedicated(pool) => pool,
                ThreadExecutor::Ambient => panic!("executor should be dedicated"),
            };
            assert!(Arc::ptr_eq(&first_pool, &cached_pool));
            value
        })
        .sum::<usize>();

    assert_eq!(total, (1usize..=128).sum::<usize>());
}

#[cfg(feature = "rayon")]
#[test]
fn thread_pool_manager_uses_global_default_for_omitted_and_zero_requests() {
    let _reset = GlobalThreadCountReset::new(2);
    let manager = ThreadPoolManager::default();

    manager
        .install(None, || 17usize)
        .expect("omitted thread request should succeed");
    let omitted_pool = match manager
        .dedicated_pool
        .read()
        .as_ref()
        .expect("global default should create a dedicated pool")
        .1
        .clone()
    {
        ThreadExecutor::Dedicated(pool) => pool,
        ThreadExecutor::Ambient => panic!("executor should be dedicated"),
    };
    assert_eq!(
        manager
            .dedicated_pool
            .read()
            .as_ref()
            .map(|(n_threads, _)| *n_threads),
        Some(2)
    );

    manager
        .install(Some(0), || 23usize)
        .expect("zero thread request should use the global default");
    let zero_pool = match manager
        .dedicated_pool
        .read()
        .as_ref()
        .expect("global default should remain cached")
        .1
        .clone()
    {
        ThreadExecutor::Dedicated(pool) => pool,
        ThreadExecutor::Ambient => panic!("executor should be dedicated"),
    };
    assert!(Arc::ptr_eq(&omitted_pool, &zero_pool));
}

#[cfg(feature = "rayon")]
#[test]
fn explicit_thread_request_overrides_global_default() {
    let _reset = GlobalThreadCountReset::new(2);
    let manager = ThreadPoolManager::default();

    manager
        .install(Some(3), || 19usize)
        .expect("explicit thread request should succeed");
    assert_eq!(
        manager
            .dedicated_pool
            .read()
            .as_ref()
            .map(|(n_threads, _)| *n_threads),
        Some(3)
    );

    manager
        .install(None, || 29usize)
        .expect("omitted thread request should fall back to the global default");
    assert_eq!(
        manager
            .dedicated_pool
            .read()
            .as_ref()
            .map(|(n_threads, _)| *n_threads),
        Some(2)
    );
}

#[test]
fn resolve_thread_request_prioritizes_explicit_positive_values() {
    let _reset = GlobalThreadCountReset::new(2);

    assert_eq!(ThreadPoolManager::resolve_thread_request(None), Some(2));
    assert_eq!(ThreadPoolManager::resolve_thread_request(Some(0)), Some(2));
    assert_eq!(ThreadPoolManager::resolve_thread_request(Some(3)), Some(3));
}

#[test]
fn set_global_thread_count_zero_resets_to_ambient_behavior() {
    let _reset = GlobalThreadCountReset::new(3);
    assert_eq!(ThreadPoolManager::global_thread_count(), Some(3));

    ThreadPoolManager::set_global_thread_count(0);
    assert_eq!(ThreadPoolManager::global_thread_count(), None);
    assert_eq!(ThreadPoolManager::resolve_thread_request(None), None);
    assert_eq!(ThreadPoolManager::resolve_thread_request(Some(0)), None);
}