use std::collections::HashMap;
use crate::optimizer::cost::config::UNKNOWN_SCAN_ROWS;
use crate::optimizer::cost::CostModelConfig;
use crate::optimizer::cost_based::ndv::estimate_group_count;
use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
use crate::planning::plan::core::nodes::PlanNodeEnum;
pub type NodeId = u64;
#[derive(Debug, Clone)]
pub struct MemoryBudgetAllocation {
pub node_id: NodeId,
pub budget_bytes: usize,
pub estimated_requirement: usize,
pub priority: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperatorImplementation {
InMemory,
External,
Hybrid,
}
#[derive(Debug, Clone)]
pub struct MemoryBudgetAllocator {
total_budget: usize,
config: CostModelConfig,
default_row_size: usize,
}
impl MemoryBudgetAllocator {
pub fn new(total_budget: usize) -> Self {
let config = CostModelConfig::default();
let default_row_size = config.row_width_bytes;
Self {
total_budget,
config,
default_row_size,
}
}
pub fn with_config(total_budget: usize, config: CostModelConfig) -> Self {
let default_row_size = config.row_width_bytes;
Self {
total_budget,
config,
default_row_size,
}
}
pub fn with_row_size(mut self, row_size: usize) -> Self {
self.default_row_size = row_size;
self
}
pub fn allocate_budget(&self, plan: &PlanNodeEnum) -> HashMap<NodeId, MemoryBudgetAllocation> {
let mut allocations = HashMap::new();
let mut requirements = Vec::new();
self.collect_requirements(plan, &mut requirements);
let total_required: usize = requirements.iter().map(|r| r.estimated_requirement).sum();
if total_required <= self.total_budget {
for req in requirements {
allocations.insert(
req.node_id,
MemoryBudgetAllocation {
node_id: req.node_id,
budget_bytes: req.estimated_requirement,
estimated_requirement: req.estimated_requirement,
priority: req.priority,
},
);
}
} else {
allocations = self.allocate_with_constraints(requirements, total_required);
}
allocations
}
fn collect_requirements(&self, plan: &PlanNodeEnum, requirements: &mut Vec<MemoryRequirement>) {
let node_id = self.get_node_id(plan);
let (requirement, priority) = self.estimate_node_memory(plan);
requirements.push(MemoryRequirement {
node_id,
estimated_requirement: requirement,
priority,
});
for child in self.get_children(plan) {
self.collect_requirements(child, requirements);
}
}
fn estimate_node_memory(&self, plan: &PlanNodeEnum) -> (usize, u32) {
self.estimate_node_memory_with_rows(plan, None)
}
fn estimate_node_memory_with_rows(
&self,
plan: &PlanNodeEnum,
row_override: Option<usize>,
) -> (usize, u32) {
let input_rows = |default_node: &PlanNodeEnum| {
row_override.unwrap_or_else(|| self.estimate_input_rows(default_node))
};
match plan {
PlanNodeEnum::Sort(node) => {
let rows = input_rows(plan);
let _ = node;
let memory = rows * self.default_row_size;
(memory, 100) }
PlanNodeEnum::InnerJoin(_) | PlanNodeEnum::LeftJoin(_) => {
let rows = input_rows(plan);
let memory = rows * self.default_row_size * 2; (memory, 90)
}
PlanNodeEnum::Aggregate(node) => {
let rows = input_rows(plan);
let groups =
estimate_group_count(rows as u64, node.group_keys().len().max(1)) as usize;
let memory = groups * self.default_row_size * 2;
(memory, 80)
}
PlanNodeEnum::Limit(node) => {
let limit = node.count() as usize;
let memory = limit * self.default_row_size;
(memory, 50) }
PlanNodeEnum::Filter(_) => {
(self.default_row_size * 10, 30)
}
PlanNodeEnum::Project(_) => {
(self.default_row_size * 10, 30)
}
PlanNodeEnum::ScanVertices(_) | PlanNodeEnum::ScanEdges(_) => {
(self.default_row_size * 10, 20)
}
PlanNodeEnum::IndexScan(_) => {
(self.default_row_size * 10, 20)
}
_ => {
(self.default_row_size * 100, 50)
}
}
}
fn estimate_input_rows(&self, _plan: &PlanNodeEnum) -> usize {
UNKNOWN_SCAN_ROWS as usize
}
pub fn allocate_budget_with_row_estimates(
&self,
plan: &PlanNodeEnum,
row_estimates: &HashMap<NodeId, u64>,
) -> HashMap<NodeId, MemoryBudgetAllocation> {
let mut allocations = HashMap::new();
let mut requirements = Vec::new();
self.collect_requirements_with_rows(plan, row_estimates, &mut requirements);
let total_required: usize = requirements.iter().map(|r| r.estimated_requirement).sum();
if total_required <= self.total_budget {
for req in requirements {
allocations.insert(
req.node_id,
MemoryBudgetAllocation {
node_id: req.node_id,
budget_bytes: req.estimated_requirement,
estimated_requirement: req.estimated_requirement,
priority: req.priority,
},
);
}
} else {
allocations = self.allocate_with_constraints(requirements, total_required);
}
allocations
}
fn collect_requirements_with_rows(
&self,
plan: &PlanNodeEnum,
row_estimates: &HashMap<NodeId, u64>,
requirements: &mut Vec<MemoryRequirement>,
) {
let node_id = self.get_node_id(plan);
let override_rows = row_estimates.get(&node_id).copied().map(|v| v as usize);
let (requirement, priority) = self.estimate_node_memory_with_rows(plan, override_rows);
requirements.push(MemoryRequirement {
node_id,
estimated_requirement: requirement,
priority,
});
for child in self.get_children(plan) {
self.collect_requirements_with_rows(child, row_estimates, requirements);
}
}
fn get_children<'a>(&self, plan: &'a PlanNodeEnum) -> Vec<&'a PlanNodeEnum> {
match plan {
PlanNodeEnum::Sort(node) => vec![node.input()],
PlanNodeEnum::InnerJoin(node) => {
vec![node.left_input(), node.right_input()]
}
PlanNodeEnum::LeftJoin(node) => {
vec![node.left_input(), node.right_input()]
}
PlanNodeEnum::Aggregate(node) => vec![node.input()],
PlanNodeEnum::Limit(node) => vec![node.input()],
PlanNodeEnum::Filter(node) => vec![node.input()],
PlanNodeEnum::Project(node) => vec![node.input()],
_ => vec![],
}
}
fn get_node_id(&self, plan: &PlanNodeEnum) -> NodeId {
plan.id() as u64
}
fn allocate_with_constraints(
&self,
requirements: Vec<MemoryRequirement>,
_total_required: usize,
) -> HashMap<NodeId, MemoryBudgetAllocation> {
let mut allocations = HashMap::new();
let total_priority: u32 = requirements.iter().map(|r| r.priority).sum();
for req in requirements {
let priority_ratio = req.priority as f64 / total_priority as f64;
let allocated = (self.total_budget as f64 * priority_ratio) as usize;
let min_allocation = if req.priority >= 80 {
req.estimated_requirement.min(self.total_budget / 4)
} else {
1024 };
let final_allocation = allocated.max(min_allocation);
allocations.insert(
req.node_id,
MemoryBudgetAllocation {
node_id: req.node_id,
budget_bytes: final_allocation,
estimated_requirement: req.estimated_requirement,
priority: req.priority,
},
);
}
allocations
}
pub fn select_operator_implementation(
&self,
plan: &PlanNodeEnum,
budget: usize,
) -> OperatorImplementation {
self.select_operator_implementation_with_rows(plan, budget, None)
}
pub fn select_operator_implementation_with_rows(
&self,
plan: &PlanNodeEnum,
budget: usize,
row_override: Option<usize>,
) -> OperatorImplementation {
let rows = |default_node: &PlanNodeEnum| {
row_override.unwrap_or_else(|| self.estimate_input_rows(default_node))
};
match plan {
PlanNodeEnum::Sort(_) => {
let required = rows(plan) * self.default_row_size;
if budget < required / 4 {
OperatorImplementation::External
} else if budget < required {
OperatorImplementation::Hybrid
} else {
OperatorImplementation::InMemory
}
}
PlanNodeEnum::InnerJoin(_) | PlanNodeEnum::LeftJoin(_) => {
let required = rows(plan) * self.default_row_size * 2;
if budget < required / 2 {
OperatorImplementation::External
} else {
OperatorImplementation::InMemory
}
}
PlanNodeEnum::Aggregate(_) => {
let required = rows(plan) * self.default_row_size;
if budget < required / 4 {
OperatorImplementation::External
} else {
OperatorImplementation::InMemory
}
}
_ => OperatorImplementation::InMemory,
}
}
pub fn can_execute_within_budget(&self, plan: &PlanNodeEnum) -> bool {
let allocations = self.allocate_budget(plan);
allocations
.values()
.all(|a| a.budget_bytes >= a.estimated_requirement / 4)
}
pub fn can_execute_within_budget_with_rows(
&self,
plan: &PlanNodeEnum,
row_estimates: &HashMap<NodeId, u64>,
) -> bool {
let allocations = self.allocate_budget_with_row_estimates(plan, row_estimates);
allocations
.values()
.all(|a| a.budget_bytes >= a.estimated_requirement / 4)
}
pub fn total_budget(&self) -> usize {
self.total_budget
}
pub fn memory_pressure_threshold(&self) -> usize {
self.config.memory_pressure_threshold
}
}
#[derive(Debug, Clone)]
struct MemoryRequirement {
node_id: NodeId,
estimated_requirement: usize,
priority: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_allocator_creation() {
let allocator = MemoryBudgetAllocator::new(100 * 1024 * 1024);
assert_eq!(allocator.total_budget(), 100 * 1024 * 1024);
}
#[test]
fn test_allocator_with_config() {
let config = CostModelConfig::default();
let allocator = MemoryBudgetAllocator::with_config(50 * 1024 * 1024, config);
assert_eq!(allocator.total_budget(), 50 * 1024 * 1024);
}
#[test]
fn test_memory_pressure_threshold() {
let config = CostModelConfig::default();
let allocator = MemoryBudgetAllocator::with_config(100 * 1024 * 1024, config);
assert_eq!(
allocator.memory_pressure_threshold(),
config.memory_pressure_threshold
);
}
#[test]
fn test_select_operator_implementation() {
let allocator = MemoryBudgetAllocator::new(100 * 1024 * 1024);
let start_node = crate::planning::plan::core::nodes::StartNode::new();
let plan = PlanNodeEnum::Start(start_node);
let impl_choice = allocator.select_operator_implementation(&plan, 1024 * 1024);
assert_eq!(impl_choice, OperatorImplementation::InMemory);
}
#[test]
fn test_can_execute_within_budget() {
let allocator = MemoryBudgetAllocator::new(100 * 1024 * 1024);
let start_node = crate::planning::plan::core::nodes::StartNode::new();
let plan = PlanNodeEnum::Start(start_node);
assert!(allocator.can_execute_within_budget(&plan));
}
}