risingwave_storage/hummock/compactor/iceberg_compaction/
memory.rs1use std::mem::size_of;
16
17use iceberg::scan::FileScanTask;
18use iceberg::spec::{DataContentType, FormatVersion, PrimitiveType, Schema, Type};
19use iceberg_compaction_core::compaction::CompactionPlan;
20
21const DATAFUSION_SORT_MERGE_RESERVATION_BYTES: usize = 10 * 1024 * 1024;
26
27const DATAFUSION_RUNTIME_FIXED_BYTES: usize = 640 * 1024;
29
30const DATAFUSION_STREAMING_DECODED_WINDOW_BYTES: usize = 16 * 1024 * 1024;
32
33const DATAFUSION_WRITER_WINDOW_BYTES: usize = 10 * 1024 * 1024;
35
36const LARGE_SORT_THRESHOLD_BYTES: usize = 64 * 1024 * 1024;
37const SORT_TEMPORARY_HEADROOM_BYTES: usize = 32 * 1024 * 1024;
38const HEAP_FIXED_HEADROOM_BYTES: usize = 256 * 1024;
39
40const COMPRESSED_TO_DECODED_INFLATION: usize = 4;
42
43const STREAMING_PREFETCH_DECODE_OVERLAP_MIN_ACTIVE_FILES: usize = 4;
45
46const EQUALITY_DELETE_INFLATION: usize = 8;
48
49const POSITION_DELETE_INFLATION: usize = 5;
51
52const HASH_JOIN_ROW_OVERHEAD_BYTES: usize = 40;
54
55const POSITION_DELETE_KEY_OVERHEAD_BYTES: usize = 32;
57
58pub fn estimate_plan_memory(
60 plan: &CompactionPlan,
61 schema: &Schema,
62 format_version: FormatVersion,
63 max_record_batch_rows: usize,
64 enable_prefetch: bool,
65 requires_sort: bool,
66) -> usize {
67 let data_files = &plan.file_group.data_files;
68 let compressed_input_bytes = sum_file_sizes(data_files.iter());
69 let prefetch = if enable_prefetch {
70 estimate_prefetch_bytes(plan, format_version)
74 } else {
75 0
76 };
77
78 let mut record_count = 0usize;
79 let mut has_complete_record_counts = true;
80 for task in data_files {
81 match task.record_count {
82 Some(count) => {
83 record_count = record_count.saturating_add(count as usize);
84 }
85 None => has_complete_record_counts = false,
86 }
87 }
88
89 let schema_row_width = estimated_schema_row_width(schema);
90 let hidden_row_width = hidden_row_width(plan, format_version);
91 let (batch_row_width, decoded_input_bytes, decoded_output_bytes) =
92 if let (Some(schema_row_width), true) = (schema_row_width, has_complete_record_counts) {
93 let row_width = schema_row_width.saturating_add(hidden_row_width);
94 (
95 row_width,
96 row_width.saturating_mul(record_count),
97 schema_row_width.saturating_mul(record_count),
98 )
99 } else {
100 let fallback_row_count = record_count.max(1);
101 let fallback_row_width = compressed_input_bytes
102 .checked_div(fallback_row_count)
103 .unwrap_or_default()
104 .saturating_mul(COMPRESSED_TO_DECODED_INFLATION)
105 .max(1);
106 let batch_row_width = fallback_row_width.saturating_add(hidden_row_width);
109 let decoded_output_bytes =
110 compressed_input_bytes.saturating_mul(COMPRESSED_TO_DECODED_INFLATION);
111 let decoded_input_bytes =
112 decoded_output_bytes.saturating_add(hidden_row_width.saturating_mul(record_count));
113 (batch_row_width, decoded_input_bytes, decoded_output_bytes)
114 };
115
116 let equality_delete_bytes = estimate_equality_delete_join_bytes(plan);
118
119 let position_delete_raw_bytes = if format_version < FormatVersion::V3 {
121 sum_file_sizes(plan.file_group.position_delete_files.iter())
122 } else {
123 0
124 };
125
126 let position_delete_join_bytes = if position_delete_raw_bytes > 0 {
127 let compressed_estimate = position_delete_raw_bytes
128 .saturating_mul(POSITION_DELETE_INFLATION)
129 .saturating_add(estimate_position_delete_join_overhead_bytes(plan));
130 compressed_input_bytes.saturating_add(compressed_estimate)
133 } else {
134 0
135 };
136
137 let executor_parallelism = plan.recommended_executor_parallelism().max(1);
138 let output_parallelism = plan.recommended_output_parallelism().max(1);
139 let sort_workspace_bytes = if requires_sort {
140 decoded_output_bytes
143 } else {
144 0
145 };
146 let sort_merge_headroom_bytes = if requires_sort {
148 DATAFUSION_SORT_MERGE_RESERVATION_BYTES.saturating_mul(output_parallelism)
149 } else {
150 0
151 };
152 let batch_allocation_bytes = batch_row_width.saturating_mul(max_record_batch_rows);
153 let concurrent_batch_allocation_bytes =
154 batch_allocation_bytes.saturating_mul(executor_parallelism);
155 let batch_overhead_bytes = if requires_sort {
158 concurrent_batch_allocation_bytes
159 } else {
160 concurrent_batch_allocation_bytes.saturating_mul(2)
161 };
162 let (retained_operator_bytes, estimated_datafusion_peak_bytes) = if requires_sort {
163 let retained_operator_bytes = sort_merge_headroom_bytes
164 .saturating_add(equality_delete_bytes)
165 .saturating_add(position_delete_join_bytes)
166 .saturating_add(prefetch)
167 .saturating_add(batch_overhead_bytes);
168 let sort_peak = sort_workspace_bytes
169 .saturating_add(sort_merge_headroom_bytes)
170 .saturating_add(equality_delete_bytes)
171 .saturating_add(position_delete_join_bytes)
172 .saturating_add(prefetch);
173 (
174 retained_operator_bytes,
175 sort_peak.max(retained_operator_bytes),
176 )
177 } else {
178 let data_file_count = data_files.len().max(1);
179 let active_files = executor_parallelism.min(data_files.len());
180 let per_partition_share = decoded_input_bytes
183 .checked_div(active_files.max(1))
184 .unwrap_or(decoded_input_bytes);
185 let active_file_share = decoded_input_bytes
186 .checked_div(data_file_count)
187 .unwrap_or(decoded_input_bytes)
188 .saturating_mul(active_files);
189 let active_file_share_with_headroom =
190 active_file_share.saturating_add(active_file_share / 4);
191 let decoded_in_flight = per_partition_share
192 .min(active_file_share_with_headroom)
193 .min(DATAFUSION_STREAMING_DECODED_WINDOW_BYTES);
194 let overlap_input_threshold =
195 DATAFUSION_STREAMING_DECODED_WINDOW_BYTES / COMPRESSED_TO_DECODED_INFLATION;
196 let scan_buffer_bytes = if active_files
197 >= STREAMING_PREFETCH_DECODE_OVERLAP_MIN_ACTIVE_FILES
198 && compressed_input_bytes >= overlap_input_threshold
199 {
200 prefetch.saturating_add(decoded_in_flight)
201 } else {
202 prefetch.max(decoded_in_flight)
203 };
204 let common_bytes = scan_buffer_bytes
205 .saturating_add(batch_overhead_bytes)
206 .saturating_add(equality_delete_bytes);
207 let retained_operator_bytes = common_bytes
208 .saturating_add(position_delete_join_bytes)
209 .max(batch_allocation_bytes)
210 .max(DATAFUSION_RUNTIME_FIXED_BYTES);
211 (retained_operator_bytes, retained_operator_bytes)
212 };
213
214 let data_file_count = data_files.len().max(1);
215 let active_data_files = executor_parallelism.min(data_files.len());
216 let active_decoded_bytes = decoded_input_bytes
217 .checked_div(data_file_count)
218 .unwrap_or(decoded_input_bytes)
219 .saturating_mul(active_data_files);
220 let active_input_bytes = estimate_prefetch_bytes(plan, format_version);
221 let total_batches = record_count
222 .saturating_add(max_record_batch_rows.saturating_sub(1))
223 .checked_div(max_record_batch_rows)
224 .unwrap_or_default();
225 let batch_overlap =
226 (total_batches as f64 / executor_parallelism.saturating_mul(8) as f64).min(1.0);
227 let batch_heap_bytes = (batch_overhead_bytes as f64 * batch_overlap) as usize;
228 let scan_heap_bytes = active_input_bytes
231 .saturating_add(active_input_bytes.min(DATAFUSION_STREAMING_DECODED_WINDOW_BYTES))
232 .saturating_add(active_decoded_bytes.min(DATAFUSION_STREAMING_DECODED_WINDOW_BYTES))
233 .saturating_add(batch_heap_bytes);
234 let writer_heap_bytes = if schema_row_width.is_some() {
235 decoded_output_bytes.saturating_mul(3) / 2
236 } else {
237 let output_scale = output_parallelism.min(4) as f64 / 4.0;
238 let large_single_scan_scale = if executor_parallelism == 1 {
239 (active_input_bytes as f64 / DATAFUSION_STREAMING_DECODED_WINDOW_BYTES as f64).min(1.0)
240 } else {
241 0.0
242 };
243 ((decoded_output_bytes / 2).min(DATAFUSION_WRITER_WINDOW_BYTES) as f64
244 * output_scale.max(large_single_scan_scale)) as usize
245 }
246 .min(DATAFUSION_WRITER_WINDOW_BYTES);
247 let streaming_heap_bytes = scan_heap_bytes.saturating_add(writer_heap_bytes);
248
249 let hidden_total_bytes = hidden_row_width.saturating_mul(record_count);
251 let position_delete_records = plan
252 .file_group
253 .position_delete_files
254 .iter()
255 .filter_map(|task| task.record_count)
256 .fold(0u64, u64::saturating_add) as usize;
257 let join_heap_bytes = if position_delete_records > 0 {
258 let delete_ratio = position_delete_records as f64 / record_count.max(1) as f64;
259 let overlap_factor = 0.6 + 0.3 * delete_ratio.min(1.0);
260 (hidden_total_bytes as f64 * overlap_factor) as usize
261 + position_delete_raw_bytes.saturating_mul(8)
262 } else if !plan.file_group.equality_delete_files.is_empty() {
263 hidden_total_bytes.saturating_add(equality_delete_bytes)
264 } else {
265 0
266 };
267 let sorted_decoded_bytes = if decoded_output_bytes > 64 * 1024 * 1024 {
270 decoded_output_bytes
271 } else {
272 decoded_output_bytes.saturating_mul(2) / 3
273 };
274 let sorted_heap_bytes = active_input_bytes
275 .saturating_add(active_input_bytes.min(DATAFUSION_STREAMING_DECODED_WINDOW_BYTES))
276 .saturating_add(sorted_decoded_bytes);
277 let execution_heap_bytes = if requires_sort {
278 sorted_heap_bytes
279 } else {
280 streaming_heap_bytes
281 };
282 let join_phase_bytes = scan_heap_bytes.saturating_add(join_heap_bytes);
283 let join_phase_bytes = if requires_sort {
284 join_phase_bytes.saturating_mul(3) / 4
285 } else {
286 join_phase_bytes
287 };
288 let large_sorted = requires_sort && decoded_output_bytes > LARGE_SORT_THRESHOLD_BYTES;
289 let fixed_heap_peak_bytes = if large_sorted {
290 scan_heap_bytes.max(join_phase_bytes)
291 } else {
292 execution_heap_bytes.max(join_phase_bytes)
293 };
294 let datafusion_peak_with_headroom = estimated_datafusion_peak_bytes
295 .saturating_add(batch_allocation_bytes)
296 .saturating_add(if requires_sort {
297 DATAFUSION_STREAMING_DECODED_WINDOW_BYTES
298 } else {
299 0
300 });
301 let datafusion_progress_peak_bytes = if large_sorted {
304 retained_operator_bytes
305 .saturating_add(active_decoded_bytes.saturating_mul(2))
306 .saturating_add(DATAFUSION_STREAMING_DECODED_WINDOW_BYTES)
307 .saturating_add(batch_allocation_bytes)
308 } else if requires_sort {
309 datafusion_peak_with_headroom
310 } else {
311 retained_operator_bytes.saturating_add(batch_allocation_bytes)
312 };
313 let estimated_datafusion_peak_bytes =
314 datafusion_peak_with_headroom.max(datafusion_progress_peak_bytes);
315
316 let heap_peak_bytes = if large_sorted {
317 fixed_heap_peak_bytes.max(
318 retained_operator_bytes
319 .saturating_add(estimated_datafusion_peak_bytes / 2)
320 .saturating_add(SORT_TEMPORARY_HEADROOM_BYTES),
321 )
322 } else if requires_sort {
323 fixed_heap_peak_bytes.max(estimated_datafusion_peak_bytes.saturating_mul(3) / 4)
324 } else {
325 fixed_heap_peak_bytes
326 }
327 .saturating_add(HEAP_FIXED_HEADROOM_BYTES);
328
329 heap_peak_bytes.saturating_add(heap_peak_bytes / 50)
330}
331
332fn estimate_prefetch_bytes(plan: &CompactionPlan, format_version: FormatVersion) -> usize {
333 let concurrency = plan.recommended_executor_parallelism().max(1);
334 let data = estimate_provider_prefetch(
335 plan.file_group.data_files.iter().filter(|task| {
336 format_version < FormatVersion::V3
337 || !task
338 .deletes
339 .iter()
340 .any(|delete| delete.file_type == DataContentType::PositionDeletes)
341 }),
342 concurrency,
343 );
344 let position_deletes = if format_version < FormatVersion::V3 {
350 estimate_provider_prefetch(plan.file_group.position_delete_files.iter(), concurrency)
351 } else {
352 0
353 };
354 let equality_deletes =
355 estimate_provider_prefetch(plan.file_group.equality_delete_files.iter(), concurrency);
356
357 data.saturating_add(position_deletes)
358 .saturating_add(equality_deletes)
359}
360
361fn estimate_provider_prefetch<'a>(
362 tasks: impl Iterator<Item = &'a FileScanTask>,
363 concurrency: usize,
364) -> usize {
365 let mut file_sizes = tasks
366 .map(|task| task.file_size_in_bytes as usize)
367 .collect::<Vec<_>>();
368 file_sizes.sort_unstable_by(|left, right| right.cmp(left));
369 file_sizes
370 .into_iter()
371 .take(concurrency)
372 .fold(0usize, usize::saturating_add)
373}
374
375fn sum_file_sizes<'a>(tasks: impl Iterator<Item = &'a FileScanTask>) -> usize {
376 tasks
377 .map(|task| task.file_size_in_bytes as usize)
378 .fold(0usize, usize::saturating_add)
379}
380
381fn estimate_position_delete_join_overhead_bytes(plan: &CompactionPlan) -> usize {
382 let Some(record_count) = complete_record_count(&plan.file_group.position_delete_files) else {
383 return 0;
384 };
385
386 HASH_JOIN_ROW_OVERHEAD_BYTES
387 .saturating_add(POSITION_DELETE_KEY_OVERHEAD_BYTES)
388 .saturating_mul(record_count)
389}
390
391fn estimate_equality_delete_join_bytes(plan: &CompactionPlan) -> usize {
392 let compressed_estimate = sum_file_sizes(plan.file_group.equality_delete_files.iter())
393 .saturating_mul(EQUALITY_DELETE_INFLATION);
394 let row_overhead = complete_record_count(&plan.file_group.equality_delete_files)
395 .unwrap_or_default()
396 .saturating_mul(HASH_JOIN_ROW_OVERHEAD_BYTES);
397
398 compressed_estimate.saturating_add(row_overhead)
399}
400
401fn complete_record_count(tasks: &[FileScanTask]) -> Option<usize> {
402 tasks.iter().try_fold(0usize, |total, task| {
403 let count = usize::try_from(task.record_count?).unwrap_or(usize::MAX);
404 Some(total.saturating_add(count))
405 })
406}
407
408fn position_delete_row_width(plan: &CompactionPlan) -> usize {
409 let max_data_path_width = plan
410 .file_group
411 .data_files
412 .iter()
413 .map(|task| task.data_file_path.len())
414 .max()
415 .unwrap_or_default();
416 max_data_path_width
417 .saturating_add(size_of::<i32>())
418 .saturating_add(size_of::<i64>())
419}
420
421fn hidden_row_width(plan: &CompactionPlan, format_version: FormatVersion) -> usize {
422 let sequence_number_width = if !plan.file_group.equality_delete_files.is_empty() {
425 size_of::<i64>()
426 } else {
427 0
428 };
429 let position_delete_width = if format_version < FormatVersion::V3
430 && !plan.file_group.position_delete_files.is_empty()
431 {
432 position_delete_row_width(plan)
433 } else {
434 0
435 };
436
437 sequence_number_width.saturating_add(position_delete_width)
438}
439
440fn estimated_schema_row_width(schema: &Schema) -> Option<usize> {
441 let mut width = 0usize;
442 for field in schema.as_struct().fields() {
443 let Type::Primitive(primitive) = field.field_type.as_ref() else {
444 return None;
445 };
446 let field_width = primitive_width(primitive)?;
447 width = width.saturating_add(field_width);
448 }
449 (width > 0).then_some(width)
450}
451
452fn primitive_width(primitive: &PrimitiveType) -> Option<usize> {
453 let width = match primitive {
454 PrimitiveType::Boolean => 1,
455 PrimitiveType::Int | PrimitiveType::Float | PrimitiveType::Date => 4,
456 PrimitiveType::Long
457 | PrimitiveType::Double
458 | PrimitiveType::Time
459 | PrimitiveType::Timestamp
460 | PrimitiveType::Timestamptz
461 | PrimitiveType::TimestampNs
462 | PrimitiveType::TimestamptzNs => 8,
463 PrimitiveType::Decimal { .. } | PrimitiveType::Uuid => 16,
464 PrimitiveType::Fixed(size) => *size as usize,
465 PrimitiveType::String | PrimitiveType::Binary => return None,
466 };
467 Some(width)
468}