use vibesql_types::DataType;
use super::morsel_debug_enabled;
#[derive(Debug, Clone)]
pub struct MorselConfig {
pub morsel_size: usize,
pub filter_size: usize,
pub group_by_size: usize,
pub join_build_size: usize,
pub join_probe_size: usize,
pub sort_size: usize,
pub scan_size: usize,
pub aggregate_size: usize,
}
pub(super) const TARGET_CACHE_BYTES: usize = 2 * 1024 * 1024;
pub(super) const MIN_MORSEL_SIZE: usize = 10_000;
pub(super) const MAX_MORSEL_SIZE: usize = 100_000;
pub(super) const DEFAULT_MORSEL_SIZE: usize = 50_000;
pub(super) const DEFAULT_FILTER_SIZE: usize = 2048;
pub(super) const DEFAULT_GROUP_BY_SIZE: usize = 2048;
pub(super) const DEFAULT_JOIN_BUILD_SIZE: usize = 4096;
pub(super) const DEFAULT_JOIN_PROBE_SIZE: usize = 4096;
pub(super) const DEFAULT_SORT_SIZE: usize = 8192;
pub(super) const DEFAULT_SCAN_SIZE: usize = 8192;
pub(super) const DEFAULT_AGGREGATE_SIZE: usize = 2048;
impl MorselConfig {
pub fn new(morsel_size: usize) -> Self {
Self {
morsel_size,
filter_size: morsel_size,
group_by_size: morsel_size,
join_build_size: morsel_size,
join_probe_size: morsel_size,
sort_size: morsel_size,
scan_size: morsel_size,
aggregate_size: morsel_size,
}
}
pub fn with_per_operation_sizes() -> Self {
Self {
morsel_size: DEFAULT_MORSEL_SIZE,
filter_size: DEFAULT_FILTER_SIZE,
group_by_size: DEFAULT_GROUP_BY_SIZE,
join_build_size: DEFAULT_JOIN_BUILD_SIZE,
join_probe_size: DEFAULT_JOIN_PROBE_SIZE,
sort_size: DEFAULT_SORT_SIZE,
scan_size: DEFAULT_SCAN_SIZE,
aggregate_size: DEFAULT_AGGREGATE_SIZE,
}
}
pub fn optimal() -> Self {
if let Ok(size_str) = std::env::var("MORSEL_SIZE") {
let morsel_size =
size_str.parse::<usize>().unwrap_or(DEFAULT_MORSEL_SIZE).clamp(1000, 500_000);
return Self::new(morsel_size);
}
Self::with_per_operation_sizes()
}
pub fn for_row_width(avg_row_bytes: usize) -> Self {
let row_bytes = avg_row_bytes.max(1);
let morsel_size = (TARGET_CACHE_BYTES / row_bytes).clamp(MIN_MORSEL_SIZE, MAX_MORSEL_SIZE);
if morsel_debug_enabled() {
eprintln!(
"[MORSEL] Adaptive sizing: {} bytes/row -> {} rows/morsel",
row_bytes, morsel_size
);
}
Self::new(morsel_size)
}
pub fn for_schema(schema: &[DataType]) -> Self {
if schema.is_empty() {
return Self::optimal();
}
const ROW_OVERHEAD: usize = 24; let row_bytes: usize =
ROW_OVERHEAD + schema.iter().map(|dt| dt.estimated_size_bytes()).sum::<usize>();
Self::for_row_width(row_bytes)
}
pub fn for_selectivity(selectivity: f64) -> Self {
let sel = selectivity.clamp(0.001, 1.0);
let adjusted = if sel < 0.1 {
((DEFAULT_MORSEL_SIZE as f64) / sel).min((MAX_MORSEL_SIZE * 2) as f64) as usize
} else {
DEFAULT_MORSEL_SIZE
};
let morsel_size = adjusted.clamp(MIN_MORSEL_SIZE, MAX_MORSEL_SIZE * 2);
if morsel_debug_enabled() {
eprintln!(
"[MORSEL] Selectivity-based sizing: {:.1}% selectivity -> {} rows/morsel",
sel * 100.0,
morsel_size
);
}
Self::new(morsel_size)
}
pub fn adaptive(schema: &[DataType], selectivity: Option<f64>) -> Self {
let base_config = Self::for_schema(schema);
match selectivity {
Some(sel) if sel < 0.1 => {
let adjusted = ((base_config.morsel_size as f64) / sel.clamp(0.001, 1.0))
.min((MAX_MORSEL_SIZE * 2) as f64) as usize;
let morsel_size = adjusted.clamp(MIN_MORSEL_SIZE, MAX_MORSEL_SIZE * 2);
if morsel_debug_enabled() {
eprintln!(
"[MORSEL] Adaptive sizing: schema={} bytes, selectivity={:.1}% -> {} rows/morsel",
schema.iter().map(|dt| dt.estimated_size_bytes()).sum::<usize>(),
sel * 100.0,
morsel_size
);
}
Self::new(morsel_size)
}
_ => base_config,
}
}
}
impl Default for MorselConfig {
fn default() -> Self {
Self::optimal()
}
}
static GLOBAL_CONFIG: std::sync::OnceLock<MorselConfig> = std::sync::OnceLock::new();
pub fn global_config() -> &'static MorselConfig {
GLOBAL_CONFIG.get_or_init(MorselConfig::optimal)
}