use std::sync::OnceLock;
#[derive(Debug, Clone, Copy)]
pub struct ParallelConfig {
#[allow(dead_code)]
pub num_threads: usize,
pub thresholds: ParallelThresholds,
}
#[derive(Debug, Clone, Copy)]
pub struct ParallelThresholds {
pub scan_filter: usize,
pub aggregate: usize,
pub join: usize,
pub sort: usize,
}
impl ParallelConfig {
pub fn global() -> Self {
static THRESHOLD_OVERRIDE: OnceLock<Option<ParallelThresholds>> = OnceLock::new();
let override_thresholds = THRESHOLD_OVERRIDE.get_or_init(|| {
std::env::var("PARALLEL_THRESHOLD").ok().map(|s| Self::parse_threshold_override(&s))
});
let num_threads = rayon::current_num_threads();
let thresholds = match override_thresholds {
Some(t) => *t,
None => Self::thresholds_for_hardware(num_threads),
};
ParallelConfig { num_threads, thresholds }
}
#[allow(dead_code)]
fn detect() -> Self {
let num_threads = rayon::current_num_threads();
if std::env::var("MORSEL_DEBUG").is_ok() {
eprintln!("[MORSEL] ParallelConfig initialized: detected {} threads", num_threads);
}
let thresholds = if let Ok(threshold_str) = std::env::var("PARALLEL_THRESHOLD") {
Self::parse_threshold_override(&threshold_str)
} else {
Self::thresholds_for_hardware(num_threads)
};
ParallelConfig { num_threads, thresholds }
}
fn parse_threshold_override(threshold_str: &str) -> ParallelThresholds {
let threshold_str = threshold_str.trim().to_lowercase();
if threshold_str == "max" || threshold_str == "disabled" {
ParallelThresholds {
scan_filter: usize::MAX,
aggregate: usize::MAX,
join: usize::MAX,
sort: usize::MAX,
}
} else if let Ok(threshold) = threshold_str.parse::<usize>() {
ParallelThresholds {
scan_filter: threshold,
aggregate: threshold,
join: threshold,
sort: threshold,
}
} else {
Self::thresholds_for_hardware(rayon::current_num_threads())
}
}
fn thresholds_for_hardware(num_threads: usize) -> ParallelThresholds {
match num_threads {
1 => ParallelThresholds {
scan_filter: usize::MAX,
aggregate: usize::MAX,
join: usize::MAX,
sort: usize::MAX,
},
2..=3 => ParallelThresholds {
scan_filter: 10_000,
aggregate: 12_500,
join: 15_000,
sort: 15_000,
},
4..=7 => ParallelThresholds {
scan_filter: 2_500,
aggregate: 3_750,
join: 5_000,
sort: 5_000,
},
_ => ParallelThresholds {
scan_filter: 1_000,
aggregate: 1_500,
join: 2_500,
sort: 2_000,
},
}
}
pub fn should_parallelize_scan(&self, row_count: usize) -> bool {
row_count >= self.thresholds.scan_filter
}
#[allow(dead_code)]
pub fn should_parallelize_aggregate(&self, row_count: usize) -> bool {
row_count >= self.thresholds.aggregate
}
#[allow(dead_code)]
pub fn should_parallelize_join(&self, row_count: usize) -> bool {
row_count >= self.thresholds.join
}
#[allow(dead_code)]
pub fn should_parallelize_sort(&self, row_count: usize) -> bool {
row_count >= self.thresholds.sort
}
}
use rayon::prelude::*;
use vibesql_storage::Row;
use super::morsel::{
global_config, morsel_parallel_filter, morsel_parallel_filter_map, morsel_parallel_map,
morsel_parallel_sort,
};
#[allow(dead_code)]
pub fn parallel_scan_filter<F>(rows: &[Row], predicate: F) -> Vec<Row>
where
F: Fn(&Row) -> bool + Sync + Send,
{
let config = ParallelConfig::global();
if config.should_parallelize_scan(rows.len()) {
rows.par_iter().filter(|row| predicate(row)).cloned().collect()
} else {
rows.iter().filter(|row| predicate(row)).cloned().collect()
}
}
#[allow(dead_code)]
pub fn parallel_scan_map<F>(rows: &[Row], transform: F) -> Vec<Row>
where
F: Fn(&Row) -> Row + Sync + Send,
{
let config = ParallelConfig::global();
if config.should_parallelize_scan(rows.len()) {
rows.par_iter().map(&transform).collect()
} else {
rows.iter().map(transform).collect()
}
}
#[allow(dead_code)]
pub fn parallel_scan_filter_map<F>(rows: &[Row], filter_map: F) -> Vec<Row>
where
F: Fn(&Row) -> Option<Row> + Sync + Send,
{
let config = ParallelConfig::global();
if config.should_parallelize_scan(rows.len()) {
rows.par_iter().filter_map(&filter_map).collect()
} else {
rows.iter().filter_map(filter_map).collect()
}
}
pub fn parallel_scan_materialize(rows: &[Row]) -> Vec<Row> {
let config = ParallelConfig::global();
if config.should_parallelize_scan(rows.len()) {
rows.par_iter().cloned().collect()
} else {
rows.to_vec()
}
}
#[allow(dead_code)]
pub fn morsel_scan_filter<F>(rows: &[Row], predicate: F) -> Vec<Row>
where
F: Fn(&Row) -> bool + Sync + Send,
{
let config = ParallelConfig::global();
if config.should_parallelize_scan(rows.len()) {
morsel_parallel_filter(rows, global_config(), predicate)
} else {
rows.iter().filter(|row| predicate(row)).cloned().collect()
}
}
#[allow(dead_code)]
pub fn morsel_scan_map<F>(rows: &[Row], transform: F) -> Vec<Row>
where
F: Fn(&Row) -> Row + Sync + Send,
{
let config = ParallelConfig::global();
if config.should_parallelize_scan(rows.len()) {
morsel_parallel_map(rows, global_config(), transform)
} else {
rows.iter().map(transform).collect()
}
}
#[allow(dead_code)]
pub fn morsel_scan_filter_map<F>(rows: &[Row], filter_map: F) -> Vec<Row>
where
F: Fn(&Row) -> Option<Row> + Sync + Send,
{
let config = ParallelConfig::global();
if config.should_parallelize_scan(rows.len()) {
morsel_parallel_filter_map(rows, global_config(), filter_map)
} else {
rows.iter().filter_map(filter_map).collect()
}
}
#[allow(dead_code)]
pub fn morsel_scan_sort<F>(rows: &[Row], compare: F) -> Vec<Row>
where
F: Fn(&Row, &Row) -> std::cmp::Ordering + Sync + Send,
{
let config = ParallelConfig::global();
if config.should_parallelize_sort(rows.len()) {
morsel_parallel_sort(rows, global_config(), compare)
} else {
let mut result = rows.to_vec();
result.sort_by(compare);
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hardware_detection() {
let config = ParallelConfig::detect();
assert!(config.num_threads >= 1);
}
#[test]
fn test_threshold_override_custom_value() {
let thresholds = ParallelConfig::parse_threshold_override("5000");
assert_eq!(thresholds.scan_filter, 5000);
assert_eq!(thresholds.aggregate, 5000);
}
#[test]
fn test_threshold_override_max() {
let thresholds = ParallelConfig::parse_threshold_override("max");
assert_eq!(thresholds.scan_filter, usize::MAX);
assert_eq!(thresholds.aggregate, usize::MAX);
}
#[test]
fn test_threshold_override_disabled() {
let thresholds = ParallelConfig::parse_threshold_override("disabled");
assert_eq!(thresholds.scan_filter, usize::MAX);
}
#[test]
fn test_single_core_never_parallelizes() {
let thresholds = ParallelConfig::thresholds_for_hardware(1);
assert_eq!(thresholds.scan_filter, usize::MAX);
}
#[test]
fn test_conservative_thresholds_2_3_cores() {
let thresholds = ParallelConfig::thresholds_for_hardware(2);
assert_eq!(thresholds.scan_filter, 10_000);
let thresholds = ParallelConfig::thresholds_for_hardware(3);
assert_eq!(thresholds.scan_filter, 10_000);
}
#[test]
fn test_moderate_thresholds_4_7_cores() {
let thresholds = ParallelConfig::thresholds_for_hardware(4);
assert_eq!(thresholds.scan_filter, 2_500);
let thresholds = ParallelConfig::thresholds_for_hardware(7);
assert_eq!(thresholds.scan_filter, 2_500);
}
#[test]
fn test_aggressive_thresholds_8_plus_cores() {
let thresholds = ParallelConfig::thresholds_for_hardware(8);
assert_eq!(thresholds.scan_filter, 1_000);
let thresholds = ParallelConfig::thresholds_for_hardware(16);
assert_eq!(thresholds.scan_filter, 1_000);
}
#[test]
fn test_should_parallelize_scan() {
let config = ParallelConfig {
num_threads: 8,
thresholds: ParallelConfig::thresholds_for_hardware(8),
};
assert!(!config.should_parallelize_scan(500));
assert!(config.should_parallelize_scan(1_000));
assert!(config.should_parallelize_scan(10_000));
}
#[test]
fn test_invalid_threshold_override_falls_back_to_auto() {
let thresholds = ParallelConfig::parse_threshold_override("invalid");
let auto_thresholds = ParallelConfig::thresholds_for_hardware(rayon::current_num_threads());
assert_eq!(thresholds.scan_filter, auto_thresholds.scan_filter);
}
fn create_test_rows(count: usize) -> Vec<Row> {
(0..count)
.map(|i| {
Row::from_vec(vec![
vibesql_types::SqlValue::Integer(i as i64),
vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from(format!("row{}", i))),
])
})
.collect()
}
#[test]
fn test_parallel_scan_filter() {
let rows = create_test_rows(100);
let filtered = parallel_scan_filter(
&rows,
|row| matches!(row.values[0], vibesql_types::SqlValue::Integer(x) if x % 2 == 0),
);
assert_eq!(filtered.len(), 50);
assert!(matches!(filtered[0].values[0], vibesql_types::SqlValue::Integer(0)));
assert!(matches!(filtered[1].values[0], vibesql_types::SqlValue::Integer(2)));
assert!(matches!(filtered[2].values[0], vibesql_types::SqlValue::Integer(4)));
}
#[test]
fn test_parallel_scan_filter_empty() {
let rows = create_test_rows(100);
let filtered = parallel_scan_filter(&rows, |_| false);
assert_eq!(filtered.len(), 0);
}
#[test]
fn test_parallel_scan_filter_all() {
let rows = create_test_rows(100);
let filtered = parallel_scan_filter(&rows, |_| true);
assert_eq!(filtered.len(), 100);
}
#[test]
fn test_parallel_scan_map() {
let rows = create_test_rows(10);
let transformed = parallel_scan_map(&rows, |row| {
let mut new_row = row.clone();
if let vibesql_types::SqlValue::Integer(x) = row.values[0] {
new_row.values[0] = vibesql_types::SqlValue::Integer(x * 2);
}
new_row
});
assert_eq!(transformed.len(), 10);
assert!(matches!(transformed[0].values[0], vibesql_types::SqlValue::Integer(0)));
assert!(matches!(transformed[1].values[0], vibesql_types::SqlValue::Integer(2)));
assert!(matches!(transformed[5].values[0], vibesql_types::SqlValue::Integer(10)));
}
#[test]
fn test_parallel_scan_filter_map() {
let rows = create_test_rows(100);
let result = parallel_scan_filter_map(&rows, |row| {
if let vibesql_types::SqlValue::Integer(x) = row.values[0] {
if x % 2 == 0 {
let mut new_row = row.clone();
new_row.values[0] = vibesql_types::SqlValue::Integer(x * 2);
return Some(new_row);
}
}
None
});
assert_eq!(result.len(), 50);
assert!(matches!(result[0].values[0], vibesql_types::SqlValue::Integer(0)));
assert!(matches!(result[1].values[0], vibesql_types::SqlValue::Integer(4))); assert!(matches!(result[2].values[0], vibesql_types::SqlValue::Integer(8)));
}
#[test]
fn test_parallel_scan_materialize() {
let rows = create_test_rows(50);
let materialized = parallel_scan_materialize(&rows);
assert_eq!(materialized.len(), 50);
for (i, row) in materialized.iter().enumerate() {
assert!(matches!(row.values[0], vibesql_types::SqlValue::Integer(x) if x == i as i64));
}
}
#[test]
fn test_parallel_scan_small_dataset_uses_sequential() {
let rows = create_test_rows(10);
let filtered = parallel_scan_filter(
&rows,
|row| matches!(row.values[0], vibesql_types::SqlValue::Integer(x) if x < 5),
);
assert_eq!(filtered.len(), 5);
}
#[test]
fn test_parallel_scan_preserves_row_structure() {
let rows = vec![
Row::from_vec(vec![
vibesql_types::SqlValue::Integer(1),
vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from("test")),
vibesql_types::SqlValue::Null,
]),
Row::from_vec(vec![
vibesql_types::SqlValue::Integer(2),
vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from("test2")),
vibesql_types::SqlValue::Double(3.5),
]),
];
let filtered = parallel_scan_filter(&rows, |_| true);
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].values.len(), 3);
assert_eq!(filtered[1].values.len(), 3);
assert!(matches!(filtered[0].values[2], vibesql_types::SqlValue::Null));
assert!(matches!(filtered[1].values[2], vibesql_types::SqlValue::Double(x) if x == 3.5));
}
}