linkrs_query/optimizer/cost_based/
memory_budget.rs1use std::collections::HashMap;
21
22use crate::optimizer::cost::config::UNKNOWN_SCAN_ROWS;
23use crate::optimizer::cost::CostModelConfig;
24use crate::optimizer::cost_based::ndv::estimate_group_count;
25use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
26use crate::planning::plan::core::nodes::PlanNodeEnum;
27
28pub type NodeId = u64;
30
31#[derive(Debug, Clone)]
33pub struct MemoryBudgetAllocation {
34 pub node_id: NodeId,
36 pub budget_bytes: usize,
38 pub estimated_requirement: usize,
40 pub priority: u32,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum OperatorImplementation {
47 InMemory,
49 External,
51 Hybrid,
53}
54
55#[derive(Debug, Clone)]
60pub struct MemoryBudgetAllocator {
61 total_budget: usize,
63 config: CostModelConfig,
65 default_row_size: usize,
67}
68
69impl MemoryBudgetAllocator {
70 pub fn new(total_budget: usize) -> Self {
72 let config = CostModelConfig::default();
73 let default_row_size = config.row_width_bytes;
74 Self {
75 total_budget,
76 config,
77 default_row_size,
78 }
79 }
80
81 pub fn with_config(total_budget: usize, config: CostModelConfig) -> Self {
83 let default_row_size = config.row_width_bytes;
84 Self {
85 total_budget,
86 config,
87 default_row_size,
88 }
89 }
90
91 pub fn with_row_size(mut self, row_size: usize) -> Self {
93 self.default_row_size = row_size;
94 self
95 }
96
97 pub fn allocate_budget(&self, plan: &PlanNodeEnum) -> HashMap<NodeId, MemoryBudgetAllocation> {
101 let mut allocations = HashMap::new();
102 let mut requirements = Vec::new();
103
104 self.collect_requirements(plan, &mut requirements);
106
107 let total_required: usize = requirements.iter().map(|r| r.estimated_requirement).sum();
109
110 if total_required <= self.total_budget {
112 for req in requirements {
114 allocations.insert(
115 req.node_id,
116 MemoryBudgetAllocation {
117 node_id: req.node_id,
118 budget_bytes: req.estimated_requirement,
119 estimated_requirement: req.estimated_requirement,
120 priority: req.priority,
121 },
122 );
123 }
124 } else {
125 allocations = self.allocate_with_constraints(requirements, total_required);
127 }
128
129 allocations
130 }
131
132 fn collect_requirements(&self, plan: &PlanNodeEnum, requirements: &mut Vec<MemoryRequirement>) {
134 let node_id = self.get_node_id(plan);
135 let (requirement, priority) = self.estimate_node_memory(plan);
136
137 requirements.push(MemoryRequirement {
138 node_id,
139 estimated_requirement: requirement,
140 priority,
141 });
142
143 for child in self.get_children(plan) {
145 self.collect_requirements(child, requirements);
146 }
147 }
148
149 fn estimate_node_memory(&self, plan: &PlanNodeEnum) -> (usize, u32) {
153 self.estimate_node_memory_with_rows(plan, None)
154 }
155
156 fn estimate_node_memory_with_rows(
160 &self,
161 plan: &PlanNodeEnum,
162 row_override: Option<usize>,
163 ) -> (usize, u32) {
164 let input_rows = |default_node: &PlanNodeEnum| {
165 row_override.unwrap_or_else(|| self.estimate_input_rows(default_node))
166 };
167 match plan {
168 PlanNodeEnum::Sort(node) => {
169 let rows = input_rows(plan);
171 let _ = node;
172 let memory = rows * self.default_row_size;
173 (memory, 100) }
175 PlanNodeEnum::InnerJoin(_) | PlanNodeEnum::LeftJoin(_) => {
176 let rows = input_rows(plan);
178 let memory = rows * self.default_row_size * 2; (memory, 90)
180 }
181 PlanNodeEnum::Aggregate(node) => {
182 let rows = input_rows(plan);
185 let groups =
186 estimate_group_count(rows as u64, node.group_keys().len().max(1)) as usize;
187 let memory = groups * self.default_row_size * 2;
188 (memory, 80)
189 }
190 PlanNodeEnum::Limit(node) => {
191 let limit = node.count() as usize;
193 let memory = limit * self.default_row_size;
194 (memory, 50) }
196 PlanNodeEnum::Filter(_) => {
197 (self.default_row_size * 10, 30)
199 }
200 PlanNodeEnum::Project(_) => {
201 (self.default_row_size * 10, 30)
203 }
204 PlanNodeEnum::ScanVertices(_) | PlanNodeEnum::ScanEdges(_) => {
205 (self.default_row_size * 10, 20)
207 }
208 PlanNodeEnum::IndexScan(_) => {
209 (self.default_row_size * 10, 20)
211 }
212 _ => {
213 (self.default_row_size * 100, 50)
215 }
216 }
217 }
218
219 fn estimate_input_rows(&self, _plan: &PlanNodeEnum) -> usize {
223 UNKNOWN_SCAN_ROWS as usize
224 }
225
226 pub fn allocate_budget_with_row_estimates(
229 &self,
230 plan: &PlanNodeEnum,
231 row_estimates: &HashMap<NodeId, u64>,
232 ) -> HashMap<NodeId, MemoryBudgetAllocation> {
233 let mut allocations = HashMap::new();
234 let mut requirements = Vec::new();
235 self.collect_requirements_with_rows(plan, row_estimates, &mut requirements);
236 let total_required: usize = requirements.iter().map(|r| r.estimated_requirement).sum();
237 if total_required <= self.total_budget {
238 for req in requirements {
239 allocations.insert(
240 req.node_id,
241 MemoryBudgetAllocation {
242 node_id: req.node_id,
243 budget_bytes: req.estimated_requirement,
244 estimated_requirement: req.estimated_requirement,
245 priority: req.priority,
246 },
247 );
248 }
249 } else {
250 allocations = self.allocate_with_constraints(requirements, total_required);
251 }
252 allocations
253 }
254
255 fn collect_requirements_with_rows(
257 &self,
258 plan: &PlanNodeEnum,
259 row_estimates: &HashMap<NodeId, u64>,
260 requirements: &mut Vec<MemoryRequirement>,
261 ) {
262 let node_id = self.get_node_id(plan);
263 let override_rows = row_estimates.get(&node_id).copied().map(|v| v as usize);
264 let (requirement, priority) = self.estimate_node_memory_with_rows(plan, override_rows);
265 requirements.push(MemoryRequirement {
266 node_id,
267 estimated_requirement: requirement,
268 priority,
269 });
270 for child in self.get_children(plan) {
271 self.collect_requirements_with_rows(child, row_estimates, requirements);
272 }
273 }
274
275 fn get_children<'a>(&self, plan: &'a PlanNodeEnum) -> Vec<&'a PlanNodeEnum> {
277 match plan {
278 PlanNodeEnum::Sort(node) => vec![node.input()],
279 PlanNodeEnum::InnerJoin(node) => {
280 vec![node.left_input(), node.right_input()]
281 }
282 PlanNodeEnum::LeftJoin(node) => {
283 vec![node.left_input(), node.right_input()]
284 }
285 PlanNodeEnum::Aggregate(node) => vec![node.input()],
286 PlanNodeEnum::Limit(node) => vec![node.input()],
287 PlanNodeEnum::Filter(node) => vec![node.input()],
288 PlanNodeEnum::Project(node) => vec![node.input()],
289 _ => vec![],
290 }
291 }
292
293 fn get_node_id(&self, plan: &PlanNodeEnum) -> NodeId {
298 plan.id() as u64
301 }
302
303 fn allocate_with_constraints(
305 &self,
306 requirements: Vec<MemoryRequirement>,
307 _total_required: usize,
308 ) -> HashMap<NodeId, MemoryBudgetAllocation> {
309 let mut allocations = HashMap::new();
310
311 let total_priority: u32 = requirements.iter().map(|r| r.priority).sum();
313
314 for req in requirements {
315 let priority_ratio = req.priority as f64 / total_priority as f64;
317 let allocated = (self.total_budget as f64 * priority_ratio) as usize;
318
319 let min_allocation = if req.priority >= 80 {
321 req.estimated_requirement.min(self.total_budget / 4)
322 } else {
323 1024 };
325
326 let final_allocation = allocated.max(min_allocation);
327
328 allocations.insert(
329 req.node_id,
330 MemoryBudgetAllocation {
331 node_id: req.node_id,
332 budget_bytes: final_allocation,
333 estimated_requirement: req.estimated_requirement,
334 priority: req.priority,
335 },
336 );
337 }
338
339 allocations
340 }
341
342 pub fn select_operator_implementation(
344 &self,
345 plan: &PlanNodeEnum,
346 budget: usize,
347 ) -> OperatorImplementation {
348 self.select_operator_implementation_with_rows(plan, budget, None)
349 }
350
351 pub fn select_operator_implementation_with_rows(
354 &self,
355 plan: &PlanNodeEnum,
356 budget: usize,
357 row_override: Option<usize>,
358 ) -> OperatorImplementation {
359 let rows = |default_node: &PlanNodeEnum| {
360 row_override.unwrap_or_else(|| self.estimate_input_rows(default_node))
361 };
362 match plan {
363 PlanNodeEnum::Sort(_) => {
364 let required = rows(plan) * self.default_row_size;
365 if budget < required / 4 {
366 OperatorImplementation::External
367 } else if budget < required {
368 OperatorImplementation::Hybrid
369 } else {
370 OperatorImplementation::InMemory
371 }
372 }
373 PlanNodeEnum::InnerJoin(_) | PlanNodeEnum::LeftJoin(_) => {
374 let required = rows(plan) * self.default_row_size * 2;
375 if budget < required / 2 {
376 OperatorImplementation::External
378 } else {
379 OperatorImplementation::InMemory
380 }
381 }
382 PlanNodeEnum::Aggregate(_) => {
383 let required = rows(plan) * self.default_row_size;
384 if budget < required / 4 {
385 OperatorImplementation::External
386 } else {
387 OperatorImplementation::InMemory
388 }
389 }
390 _ => OperatorImplementation::InMemory,
391 }
392 }
393
394 pub fn can_execute_within_budget(&self, plan: &PlanNodeEnum) -> bool {
396 let allocations = self.allocate_budget(plan);
397 allocations
398 .values()
399 .all(|a| a.budget_bytes >= a.estimated_requirement / 4)
400 }
401
402 pub fn can_execute_within_budget_with_rows(
404 &self,
405 plan: &PlanNodeEnum,
406 row_estimates: &HashMap<NodeId, u64>,
407 ) -> bool {
408 let allocations = self.allocate_budget_with_row_estimates(plan, row_estimates);
409 allocations
410 .values()
411 .all(|a| a.budget_bytes >= a.estimated_requirement / 4)
412 }
413
414 pub fn total_budget(&self) -> usize {
416 self.total_budget
417 }
418
419 pub fn memory_pressure_threshold(&self) -> usize {
421 self.config.memory_pressure_threshold
422 }
423}
424
425#[derive(Debug, Clone)]
427struct MemoryRequirement {
428 node_id: NodeId,
429 estimated_requirement: usize,
430 priority: u32,
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_allocator_creation() {
439 let allocator = MemoryBudgetAllocator::new(100 * 1024 * 1024);
440 assert_eq!(allocator.total_budget(), 100 * 1024 * 1024);
441 }
442
443 #[test]
444 fn test_allocator_with_config() {
445 let config = CostModelConfig::default();
446 let allocator = MemoryBudgetAllocator::with_config(50 * 1024 * 1024, config);
447 assert_eq!(allocator.total_budget(), 50 * 1024 * 1024);
448 }
449
450 #[test]
451 fn test_memory_pressure_threshold() {
452 let config = CostModelConfig::default();
453 let allocator = MemoryBudgetAllocator::with_config(100 * 1024 * 1024, config);
454 assert_eq!(
455 allocator.memory_pressure_threshold(),
456 config.memory_pressure_threshold
457 );
458 }
459
460 #[test]
461 fn test_select_operator_implementation() {
462 let allocator = MemoryBudgetAllocator::new(100 * 1024 * 1024);
463
464 let start_node = crate::planning::plan::core::nodes::StartNode::new();
466 let plan = PlanNodeEnum::Start(start_node);
467
468 let impl_choice = allocator.select_operator_implementation(&plan, 1024 * 1024);
470 assert_eq!(impl_choice, OperatorImplementation::InMemory);
471 }
472
473 #[test]
474 fn test_can_execute_within_budget() {
475 let allocator = MemoryBudgetAllocator::new(100 * 1024 * 1024);
476
477 let start_node = crate::planning::plan::core::nodes::StartNode::new();
478 let plan = PlanNodeEnum::Start(start_node);
479
480 assert!(allocator.can_execute_within_budget(&plan));
482 }
483}