Skip to main content

risingwave_storage/hummock/compactor/
compaction_utils.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{BTreeMap, HashMap};
16use std::marker::PhantomData;
17use std::ops::Bound;
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU64, Ordering};
20
21use await_tree::{InstrumentAwait, SpanExt};
22use bytes::Bytes;
23use itertools::Itertools;
24use risingwave_common::catalog::TableId;
25use risingwave_common::config::meta::default::compaction_config;
26use risingwave_common::constants::hummock::CompactionFilterFlag;
27use risingwave_hummock_sdk::compact_task::CompactTask;
28use risingwave_hummock_sdk::key::FullKey;
29use risingwave_hummock_sdk::key_range::KeyRange;
30use risingwave_hummock_sdk::sstable_info::SstableInfo;
31use risingwave_hummock_sdk::table_stats::TableStatsMap;
32use risingwave_hummock_sdk::{EpochWithGap, KeyComparator, can_concat};
33use risingwave_pb::hummock::compact_task::PbTaskType;
34use risingwave_pb::hummock::{
35    PbLevelType, PbSstableFilterLayout, PbSstableFilterType, PbTableSchema,
36};
37use tokio::time::Instant;
38
39pub use super::context::CompactorContext;
40use crate::compaction_catalog_manager::CompactionCatalogAgentRef;
41use crate::hummock::compactor::{
42    ConcatSstableIterator, MultiCompactionFilter, TaskProgress, TtlCompactionFilter,
43};
44use crate::hummock::iterator::{
45    Forward, HummockIterator, MergeIterator, NonPkPrefixSkipWatermarkIterator,
46    NonPkPrefixSkipWatermarkState, PkPrefixSkipWatermarkIterator, PkPrefixSkipWatermarkState,
47    UserIterator,
48};
49use crate::hummock::multi_builder::TableBuilderFactory;
50use crate::hummock::{
51    CachePolicy, FilterBuilder, GetObjectId, HummockResult, MemoryLimiter, SstableBuilder,
52    SstableBuilderOptions, SstableWriterFactory, SstableWriterOptions,
53};
54use crate::monitor::StoreLocalStatistic;
55
56pub struct RemoteBuilderFactory<W: SstableWriterFactory, F: FilterBuilder> {
57    pub object_id_getter: Arc<dyn GetObjectId>,
58    pub limiter: Arc<MemoryLimiter>,
59    pub options: SstableBuilderOptions,
60    pub policy: CachePolicy,
61    pub remote_rpc_cost: Arc<AtomicU64>,
62    pub compaction_catalog_agent_ref: CompactionCatalogAgentRef,
63    pub sstable_writer_factory: W,
64    pub _phantom: PhantomData<F>,
65}
66
67#[async_trait::async_trait]
68impl<W: SstableWriterFactory, F: FilterBuilder> TableBuilderFactory for RemoteBuilderFactory<W, F> {
69    type Filter = F;
70    type Writer = W::Writer;
71
72    async fn open_builder(&mut self) -> HummockResult<SstableBuilder<Self::Writer, Self::Filter>> {
73        let timer = Instant::now();
74        let table_id = self
75            .object_id_getter
76            .get_new_sst_object_id()
77            .instrument_await("remote_builder_get_new_sst_object_id".verbose())
78            .await?;
79        let cost = (timer.elapsed().as_secs_f64() * 1000000.0).round() as u64;
80        self.remote_rpc_cost.fetch_add(cost, Ordering::Relaxed);
81        let writer_options = SstableWriterOptions {
82            capacity_hint: Some(self.options.capacity + self.options.block_capacity),
83            tracker: None,
84            policy: self.policy,
85        };
86        let writer = self
87            .sstable_writer_factory
88            .create_sst_writer(table_id, writer_options)
89            .instrument_await("remote_builder_create_sst_writer".verbose())
90            .await?;
91        let builder = SstableBuilder::new(
92            table_id,
93            writer,
94            Self::Filter::create(self.options.filter_builder_options()),
95            self.options.clone(),
96            self.compaction_catalog_agent_ref.clone(),
97            Some(self.limiter.clone()),
98        );
99        Ok(builder)
100    }
101}
102
103/// `CompactionStatistics` will count the results of each compact split
104#[derive(Default, Debug)]
105pub struct CompactionStatistics {
106    // to report per-table metrics
107    pub delta_drop_stat: TableStatsMap,
108
109    // to calculate delete ratio
110    pub iter_total_key_counts: u64,
111    pub iter_drop_key_counts: u64,
112}
113
114#[derive(Clone, Default)]
115pub struct TaskConfig {
116    pub(crate) key_range: KeyRange,
117    pub(crate) cache_policy: CachePolicy,
118    pub(crate) gc_delete_keys: bool,
119    pub(crate) retain_multiple_version: bool,
120    /// Resolved output SST filter layout from `CompactTask::sstable_filter_layout_for_output`.
121    pub(crate) sstable_filter_layout: PbSstableFilterLayout,
122    pub(crate) sstable_filter_type: PbSstableFilterType,
123
124    pub(crate) table_vnode_partition: BTreeMap<TableId, u32>,
125    /// `TableId` -> `TableSchema`
126    /// Schemas in `table_schemas` are at least as new as the one used to create `input_ssts`.
127    /// For a table with schema existing in `table_schemas`, its columns not in `table_schemas` but in `input_ssts` can be safely dropped.
128    pub(crate) table_schemas: HashMap<TableId, PbTableSchema>,
129    /// `disable_drop_column_optimization` should only be set in benchmark.
130    pub(crate) disable_drop_column_optimization: bool,
131}
132
133impl TaskConfig {
134    /// Construct a compact task config for tests and benchmarks.
135    pub fn for_test(
136        key_range: KeyRange,
137        cache_policy: CachePolicy,
138        gc_delete_keys: bool,
139        sstable_filter_layout: PbSstableFilterLayout,
140        table_schemas: HashMap<TableId, PbTableSchema>,
141    ) -> Self {
142        Self {
143            key_range,
144            cache_policy,
145            gc_delete_keys,
146            retain_multiple_version: false,
147            sstable_filter_layout,
148            sstable_filter_type: PbSstableFilterType::SstableFilterXor16,
149            table_vnode_partition: BTreeMap::default(),
150            table_schemas,
151            disable_drop_column_optimization: false,
152        }
153    }
154
155    pub fn with_disable_drop_column_optimization(mut self, disable: bool) -> Self {
156        self.disable_drop_column_optimization = disable;
157        self
158    }
159}
160
161pub fn build_multi_compaction_filter(compact_task: &CompactTask) -> MultiCompactionFilter {
162    let mut multi_filter = MultiCompactionFilter::default();
163    let compaction_filter_flag =
164        CompactionFilterFlag::from_bits(compact_task.compaction_filter_mask).unwrap_or_default();
165    // STATE_CLEAN is handled by compact-task table-id normalization and iterator-level block
166    // pruning. Keep the flag as a no-op here for compatibility with existing task configs.
167
168    if compaction_filter_flag.contains(CompactionFilterFlag::TTL) {
169        let id_to_ttl = compact_task
170            .table_options
171            .iter()
172            .filter_map(|(id, option)| {
173                option
174                    .retention_seconds
175                    .and_then(|ttl| if ttl > 0 { Some((*id, ttl)) } else { None })
176            })
177            .collect();
178
179        let ttl_filter = Box::new(TtlCompactionFilter::new(
180            id_to_ttl,
181            compact_task.current_epoch_time,
182        ));
183        multi_filter.register(ttl_filter);
184    }
185
186    multi_filter
187}
188
189fn generate_splits_fast(
190    sstable_infos: &[&SstableInfo],
191    compaction_size: u64,
192    context: &CompactorContext,
193    max_sub_compaction: u32,
194) -> Vec<KeyRange> {
195    let worker_num = context.compaction_executor.worker_num();
196    let parallel_compact_size = (context.storage_opts.parallel_compact_size_mb as u64) << 20;
197
198    let parallelism = calculate_task_parallelism_impl(
199        worker_num,
200        parallel_compact_size,
201        compaction_size,
202        max_sub_compaction,
203    );
204    let mut indexes = vec![];
205    for sst in sstable_infos {
206        let key_range = &sst.key_range;
207        indexes.push(
208            FullKey {
209                user_key: FullKey::decode(&key_range.left).user_key,
210                epoch_with_gap: EpochWithGap::new_max_epoch(),
211            }
212            .encode(),
213        );
214        indexes.push(
215            FullKey {
216                user_key: FullKey::decode(&key_range.right).user_key,
217                epoch_with_gap: EpochWithGap::new_max_epoch(),
218            }
219            .encode(),
220        );
221    }
222    indexes.sort_by(|a, b| KeyComparator::compare_encoded_full_key(a.as_ref(), b.as_ref()));
223    indexes.dedup();
224    if indexes.len() <= parallelism {
225        return vec![];
226    }
227
228    let mut splits = vec![];
229    splits.push(KeyRange::default());
230    let parallel_key_count = indexes.len() / parallelism;
231    let mut last_split_key_count = 0;
232    for key in indexes {
233        if last_split_key_count >= parallel_key_count {
234            splits.last_mut().unwrap().right = Bytes::from(key.clone());
235            splits.push(KeyRange::new(Bytes::from(key.clone()), Bytes::default()));
236            last_split_key_count = 0;
237        }
238        last_split_key_count += 1;
239    }
240
241    splits
242}
243
244pub async fn generate_splits(
245    sstable_infos: &[&SstableInfo],
246    compaction_size: u64,
247    context: &CompactorContext,
248    max_sub_compaction: u32,
249) -> HummockResult<Vec<KeyRange>> {
250    let parallel_compact_size = (context.storage_opts.parallel_compact_size_mb as u64) << 20;
251    if compaction_size > parallel_compact_size {
252        if sstable_infos.len() > context.storage_opts.compactor_max_preload_meta_file_count {
253            return Ok(generate_splits_fast(
254                sstable_infos,
255                compaction_size,
256                context,
257                max_sub_compaction,
258            ));
259        }
260        let mut indexes = vec![];
261        // preload the meta and get the smallest key to split sub_compaction
262        for sstable_info in sstable_infos {
263            let sstable = context
264                .sstable_store
265                .sstable(sstable_info, &mut StoreLocalStatistic::default())
266                .await?;
267            indexes.extend(sstable.meta.block_metas.iter().map(|block| {
268                let data_size = block.len;
269                let full_key = FullKey {
270                    user_key: FullKey::decode(&block.smallest_key).user_key,
271                    epoch_with_gap: EpochWithGap::new_max_epoch(),
272                }
273                .encode();
274                (data_size as u64, full_key)
275            }));
276        }
277        // sort by key, as for every data block has the same size;
278        indexes.sort_by(|a, b| KeyComparator::compare_encoded_full_key(a.1.as_ref(), b.1.as_ref()));
279        let mut splits = vec![];
280        splits.push(KeyRange::default());
281
282        let parallelism = calculate_task_parallelism_impl(
283            context.compaction_executor.worker_num(),
284            parallel_compact_size,
285            compaction_size,
286            max_sub_compaction,
287        );
288
289        let sub_compaction_data_size =
290            std::cmp::max(compaction_size / parallelism as u64, parallel_compact_size);
291
292        if parallelism > 1 {
293            let mut last_buffer_size = 0;
294            let mut last_key: Vec<u8> = vec![];
295            let mut remaining_size = indexes.iter().map(|block| block.0).sum::<u64>();
296            for (data_size, key) in indexes {
297                if last_buffer_size >= sub_compaction_data_size
298                    && !last_key.eq(&key)
299                    && remaining_size > parallel_compact_size
300                {
301                    splits.last_mut().unwrap().right = Bytes::from(key.clone());
302                    splits.push(KeyRange::new(Bytes::from(key.clone()), Bytes::default()));
303                    last_buffer_size = data_size;
304                } else {
305                    last_buffer_size += data_size;
306                }
307                remaining_size -= data_size;
308                last_key = key;
309            }
310            return Ok(splits);
311        }
312    }
313
314    Ok(vec![])
315}
316
317pub fn estimate_task_output_capacity(context: CompactorContext, task: &CompactTask) -> usize {
318    let max_target_file_size = context.storage_opts.sstable_size_mb as usize * (1 << 20);
319    let total_input_uncompressed_file_size = task
320        .read_input_ssts()
321        .map(|table| table.uncompressed_file_size)
322        .sum::<u64>();
323
324    let capacity = std::cmp::min(task.target_file_size as usize, max_target_file_size);
325    std::cmp::min(capacity, total_input_uncompressed_file_size as usize)
326}
327
328pub fn estimate_output_key_count_by_size(
329    total_key_count: u64,
330    total_size: u64,
331    output_capacity: usize,
332) -> usize {
333    if total_key_count == 0 {
334        return 0;
335    }
336    if total_size == 0 {
337        return total_key_count.try_into().unwrap_or(usize::MAX);
338    }
339    if output_capacity == 0 {
340        return 0;
341    }
342
343    // Estimate how many keys one target-sized output SST will contain by using the input key
344    // density. This replaces the old task-level key-count heuristic for filter layout decisions:
345    // large tasks can now still produce plain-filter output SSTs when each output is small.
346    let estimated = (total_key_count as u128 * output_capacity as u128)
347        .div_ceil(total_size as u128)
348        .min(total_key_count as u128);
349    estimated.try_into().unwrap_or(usize::MAX)
350}
351
352pub fn estimate_output_key_count_for_input_ssts<'a>(
353    input_ssts: impl Iterator<Item = &'a SstableInfo>,
354    output_capacity: usize,
355) -> usize {
356    let (total_key_count, total_uncompressed_size) =
357        input_ssts.fold((0u64, 0u64), |(key_count, size), sst| {
358            (
359                key_count + sst.total_key_count,
360                size + sst.uncompressed_file_size,
361            )
362        });
363
364    estimate_output_key_count_by_size(total_key_count, total_uncompressed_size, output_capacity)
365}
366
367pub fn estimate_output_key_count_for_task(task: &CompactTask, output_capacity: usize) -> usize {
368    estimate_output_key_count_for_input_ssts(task.read_input_ssts(), output_capacity)
369}
370
371pub fn blocked_xor_filter_key_count_threshold(
372    blocked_xor_filter_kv_count_threshold: Option<u64>,
373) -> usize {
374    blocked_xor_filter_kv_count_threshold
375        .unwrap_or(compaction_config::DEFAULT_BLOCKED_XOR_FILTER_KV_COUNT_THRESHOLD)
376        .try_into()
377        .unwrap_or(usize::MAX)
378}
379
380/// Compare result of compaction task and input. The data saw by user shall not change after applying compaction result.
381pub async fn check_compaction_result(
382    compact_task: &CompactTask,
383    context: CompactorContext,
384    compaction_catalog_agent_ref: CompactionCatalogAgentRef,
385) -> HummockResult<bool> {
386    // This check method does not consider dropped keys by compaction filter.
387    if compact_task.contains_ttl() {
388        return Ok(true);
389    }
390
391    let mut table_iters = Vec::new();
392
393    for level in &compact_task.input_ssts {
394        if level.level_type == PbLevelType::Nonoverlapping {
395            let tables = level.read_sstable_infos().cloned().collect_vec();
396            if tables.is_empty() {
397                continue;
398            }
399            debug_assert!(can_concat(&tables));
400
401            table_iters.push(ConcatSstableIterator::new(
402                tables,
403                KeyRange::inf(),
404                context.sstable_store.clone(),
405                Arc::new(TaskProgress::default()),
406                context.storage_opts.compactor_iter_max_io_retry_times,
407            ));
408        } else {
409            for table_info in level.read_sstable_infos().cloned() {
410                table_iters.push(ConcatSstableIterator::new(
411                    vec![table_info],
412                    KeyRange::inf(),
413                    context.sstable_store.clone(),
414                    Arc::new(TaskProgress::default()),
415                    context.storage_opts.compactor_iter_max_io_retry_times,
416                ));
417            }
418        }
419    }
420
421    let iter = MergeIterator::for_compactor(table_iters);
422    let left_iter = {
423        let skip_watermark_iter = PkPrefixSkipWatermarkIterator::new(
424            iter,
425            PkPrefixSkipWatermarkState::from_safe_epoch_watermarks(
426                compact_task.pk_prefix_table_watermarks.clone(),
427            ),
428        );
429
430        let combine_iter = NonPkPrefixSkipWatermarkIterator::new(
431            skip_watermark_iter,
432            NonPkPrefixSkipWatermarkState::from_safe_epoch_watermarks(
433                compact_task.non_pk_prefix_table_watermarks.clone(),
434                compaction_catalog_agent_ref.clone(),
435            ),
436        );
437
438        UserIterator::new(
439            combine_iter,
440            (Bound::Unbounded, Bound::Unbounded),
441            u64::MAX,
442            0,
443            None,
444        )
445    };
446    let iter = ConcatSstableIterator::new(
447        compact_task.sorted_output_ssts.clone(),
448        KeyRange::inf(),
449        context.sstable_store.clone(),
450        Arc::new(TaskProgress::default()),
451        context.storage_opts.compactor_iter_max_io_retry_times,
452    );
453    let right_iter = {
454        let skip_watermark_iter = PkPrefixSkipWatermarkIterator::new(
455            iter,
456            PkPrefixSkipWatermarkState::from_safe_epoch_watermarks(
457                compact_task.pk_prefix_table_watermarks.clone(),
458            ),
459        );
460
461        let combine_iter = NonPkPrefixSkipWatermarkIterator::new(
462            skip_watermark_iter,
463            NonPkPrefixSkipWatermarkState::from_safe_epoch_watermarks(
464                compact_task.non_pk_prefix_table_watermarks.clone(),
465                compaction_catalog_agent_ref,
466            ),
467        );
468
469        UserIterator::new(
470            combine_iter,
471            (Bound::Unbounded, Bound::Unbounded),
472            u64::MAX,
473            0,
474            None,
475        )
476    };
477
478    check_result(left_iter, right_iter).await
479}
480
481pub async fn check_flush_result<I: HummockIterator<Direction = Forward>>(
482    left_iter: UserIterator<I>,
483    sort_ssts: Vec<SstableInfo>,
484    context: CompactorContext,
485) -> HummockResult<bool> {
486    let iter = ConcatSstableIterator::new(
487        sort_ssts,
488        KeyRange::inf(),
489        context.sstable_store.clone(),
490        Arc::new(TaskProgress::default()),
491        0,
492    );
493    let right_iter = UserIterator::new(
494        iter,
495        (Bound::Unbounded, Bound::Unbounded),
496        u64::MAX,
497        0,
498        None,
499    );
500    check_result(left_iter, right_iter).await
501}
502
503async fn check_result<
504    I1: HummockIterator<Direction = Forward>,
505    I2: HummockIterator<Direction = Forward>,
506>(
507    mut left_iter: UserIterator<I1>,
508    mut right_iter: UserIterator<I2>,
509) -> HummockResult<bool> {
510    left_iter.rewind().await?;
511    right_iter.rewind().await?;
512    let mut right_count = 0;
513    let mut left_count = 0;
514    while left_iter.is_valid() && right_iter.is_valid() {
515        if left_iter.key() != right_iter.key() {
516            tracing::error!(
517                "The key of input and output not equal. key: {:?} vs {:?}",
518                left_iter.key(),
519                right_iter.key()
520            );
521            return Ok(false);
522        }
523        if left_iter.value() != right_iter.value() {
524            tracing::error!(
525                "The value of input and output not equal. key: {:?}, value: {:?} vs {:?}",
526                left_iter.key(),
527                left_iter.value(),
528                right_iter.value()
529            );
530            return Ok(false);
531        }
532        left_iter.next().await?;
533        right_iter.next().await?;
534        left_count += 1;
535        right_count += 1;
536    }
537    while left_iter.is_valid() {
538        left_count += 1;
539        left_iter.next().await?;
540    }
541    while right_iter.is_valid() {
542        right_count += 1;
543        right_iter.next().await?;
544    }
545    if left_count != right_count {
546        tracing::error!(
547            "The key count of input and output not equal: {} vs {}",
548            left_count,
549            right_count
550        );
551        return Ok(false);
552    }
553    Ok(true)
554}
555
556pub fn optimize_by_copy_block(compact_task: &CompactTask, context: &CompactorContext) -> bool {
557    let input_ssts = compact_task.read_input_ssts().collect_vec();
558    let compaction_size = input_ssts_size(&input_ssts);
559    optimize_by_copy_block_with_input(compact_task, context, &input_ssts, compaction_size)
560}
561
562fn optimize_by_copy_block_with_input(
563    compact_task: &CompactTask,
564    context: &CompactorContext,
565    input_ssts: &[&SstableInfo],
566    compaction_size: u64,
567) -> bool {
568    let all_ssts_are_blocked_filter = input_ssts
569        .iter()
570        .all(|table_info| table_info.filter_layout == PbSstableFilterLayout::Blocked);
571    let current_filter_type = compact_task.sstable_filter_type;
572    let all_ssts_match_filter_type = input_ssts
573        .iter()
574        .all(|table_info| table_info.filter_type == current_filter_type);
575    // Fast compaction path can only preserve blocked filters by copying block payloads (and their
576    // per-block filter bytes). If the output-SST-level heuristic now wants a plain filter, fall back
577    // to the normal compaction path to rebuild filters. This intentionally lets tasks that were
578    // previously classified as blocked by total task key count move back to plain output filters.
579    let output_capacity = estimate_task_output_capacity(context.clone(), compact_task);
580    let estimated_output_key_count =
581        estimate_output_key_count_for_input_ssts(input_ssts.iter().copied(), output_capacity);
582    let output_filter_layout =
583        compact_task.sstable_filter_layout_for_output(estimated_output_key_count as u64);
584
585    let delete_key_count = input_ssts
586        .iter()
587        .map(|table_info| table_info.stale_key_count + table_info.range_tombstone_count)
588        .sum::<u64>();
589    let total_key_count = input_ssts
590        .iter()
591        .map(|table_info| table_info.total_key_count)
592        .sum::<u64>();
593
594    let single_table = compact_task.get_table_ids_from_input_ssts().count() == 1;
595    context.storage_opts.enable_fast_compaction
596        && matches!(
597            current_filter_type,
598            PbSstableFilterType::SstableFilterXor8 | PbSstableFilterType::SstableFilterXor16
599        )
600        && all_ssts_are_blocked_filter
601        && all_ssts_match_filter_type
602        && output_filter_layout == PbSstableFilterLayout::Blocked
603        && !compact_task.contains_range_tombstone()
604        && !compact_task.contains_ttl()
605        && !compact_task.contains_split_sst()
606        && single_table
607        && compact_task.target_level > 0
608        && compact_task.input_ssts.len() == 2
609        && compaction_size < context.storage_opts.compactor_fast_max_compact_task_size
610        && delete_key_count * 100
611            < context.storage_opts.compactor_fast_max_compact_delete_ratio as u64 * total_key_count
612        && compact_task.task_type == PbTaskType::Dynamic
613}
614
615pub async fn generate_splits_for_task(
616    compact_task: &mut CompactTask,
617    context: &CompactorContext,
618    optimize_by_copy_block: bool,
619) -> HummockResult<()> {
620    let input_ssts = compact_task.read_input_ssts().collect_vec();
621    let compaction_size = input_ssts_size(&input_ssts);
622
623    if !optimize_by_copy_block {
624        let splits = generate_splits(
625            &input_ssts,
626            compaction_size,
627            context,
628            compact_task.max_sub_compaction,
629        )
630        .await?;
631        if !splits.is_empty() {
632            compact_task.splits = splits;
633        }
634        return Ok(());
635    }
636
637    Ok(())
638}
639
640pub fn metrics_report_for_task(compact_task: &CompactTask, context: &CompactorContext) {
641    let group_label = compact_task.compaction_group_id.to_string();
642    let cur_level_label = compact_task.input_ssts[0].level_idx.to_string();
643
644    let (select_size, select_count) = read_sstable_size_and_count(
645        compact_task
646            .input_ssts
647            .iter()
648            .filter(|level| level.level_idx != compact_task.target_level)
649            .flat_map(|level| level.read_sstable_infos()),
650    );
651    let (target_level_read_bytes, target_count) = read_sstable_size_and_count(
652        compact_task
653            .input_ssts
654            .iter()
655            .filter(|level| level.level_idx == compact_task.target_level)
656            .flat_map(|level| level.read_sstable_infos()),
657    );
658
659    context
660        .compactor_metrics
661        .compact_read_current_level
662        .with_label_values(&[&group_label, &cur_level_label])
663        .inc_by(select_size);
664    context
665        .compactor_metrics
666        .compact_read_sstn_current_level
667        .with_label_values(&[&group_label, &cur_level_label])
668        .inc_by(select_count as u64);
669
670    let next_level_label = compact_task.target_level.to_string();
671    context
672        .compactor_metrics
673        .compact_read_next_level
674        .with_label_values(&[&group_label, &next_level_label])
675        .inc_by(target_level_read_bytes);
676    context
677        .compactor_metrics
678        .compact_read_sstn_next_level
679        .with_label_values(&[&group_label, &next_level_label])
680        .inc_by(target_count as u64);
681}
682
683fn read_sstable_size_and_count<'a>(
684    sstable_infos: impl IntoIterator<Item = &'a SstableInfo>,
685) -> (u64, usize) {
686    sstable_infos
687        .into_iter()
688        .fold((0, 0), |(size, count), table_info| {
689            (size + table_info.sst_size, count + 1)
690        })
691}
692
693pub fn calculate_task_parallelism(compact_task: &CompactTask, context: &CompactorContext) -> usize {
694    let input_ssts = compact_task.read_input_ssts().collect_vec();
695    let compaction_size = input_ssts_size(&input_ssts);
696    let optimize_by_copy_block =
697        optimize_by_copy_block_with_input(compact_task, context, &input_ssts, compaction_size);
698
699    if optimize_by_copy_block {
700        return 1;
701    }
702
703    let parallel_compact_size = (context.storage_opts.parallel_compact_size_mb as u64) << 20;
704    calculate_task_parallelism_impl(
705        context.compaction_executor.worker_num(),
706        parallel_compact_size,
707        compaction_size,
708        compact_task.max_sub_compaction,
709    )
710}
711
712fn input_ssts_size(input_ssts: &[&SstableInfo]) -> u64 {
713    input_ssts
714        .iter()
715        .map(|table_info| table_info.sst_size)
716        .sum()
717}
718
719pub fn calculate_task_parallelism_impl(
720    worker_num: usize,
721    parallel_compact_size: u64,
722    compaction_size: u64,
723    max_sub_compaction: u32,
724) -> usize {
725    let parallelism = compaction_size.div_ceil(parallel_compact_size);
726    worker_num.min(parallelism.min(max_sub_compaction as u64) as usize)
727}
728
729#[cfg(test)]
730mod tests {
731    use std::sync::Arc;
732
733    use risingwave_common::catalog::TableId;
734    use risingwave_hummock_sdk::level::InputLevel;
735    use risingwave_hummock_sdk::sstable_info::SstableInfoInner;
736    use risingwave_pb::hummock::compact_task::PbTaskType;
737    use risingwave_pb::hummock::{PbLevelType, PbSstableFilterLayout, PbSstableFilterType};
738
739    use super::{
740        CompactTask, CompactorContext, estimate_output_key_count_by_size, optimize_by_copy_block,
741    };
742    use crate::hummock::compactor::new_compaction_await_tree_reg_ref;
743    use crate::hummock::iterator::test_utils::mock_sstable_store;
744    use crate::monitor::CompactorMetrics;
745    use crate::opts::StorageOpts;
746
747    fn test_sstable(
748        table_id: TableId,
749        total_key_count: u64,
750        filter_type: PbSstableFilterType,
751    ) -> risingwave_hummock_sdk::sstable_info::SstableInfo {
752        SstableInfoInner {
753            object_id: 1.into(),
754            sst_id: 1.into(),
755            table_ids: vec![table_id],
756            total_key_count,
757            sst_size: 1024,
758            uncompressed_file_size: 1024,
759            filter_type,
760            filter_layout: PbSstableFilterLayout::Blocked,
761            ..Default::default()
762        }
763        .into()
764    }
765
766    async fn test_context() -> CompactorContext {
767        CompactorContext::new_local_compact_context(
768            Arc::new(StorageOpts::default()),
769            mock_sstable_store().await,
770            Arc::new(CompactorMetrics::unused()),
771            Some(new_compaction_await_tree_reg_ref(
772                await_tree::Config::default(),
773            )),
774        )
775    }
776
777    fn test_compact_task(
778        layout: PbSstableFilterLayout,
779        blocked_xor_filter_kv_count_threshold: Option<u64>,
780        filter_type: PbSstableFilterType,
781    ) -> CompactTask {
782        let table_id = TableId::new(1);
783        CompactTask {
784            input_ssts: vec![
785                InputLevel {
786                    level_idx: 1,
787                    level_type: PbLevelType::Nonoverlapping,
788                    table_infos: vec![test_sstable(table_id, 10, filter_type)],
789                },
790                InputLevel {
791                    level_idx: 2,
792                    level_type: PbLevelType::Nonoverlapping,
793                    table_infos: vec![test_sstable(table_id, 10, filter_type)],
794                },
795            ],
796            existing_table_ids: vec![table_id],
797            target_level: 2,
798            target_file_size: 1024,
799            task_type: PbTaskType::Dynamic,
800            sstable_filter_type: filter_type,
801            sstable_filter_layout: layout,
802            blocked_xor_filter_kv_count_threshold,
803            ..Default::default()
804        }
805    }
806
807    #[test]
808    fn test_estimate_output_key_count_by_size_scales_to_output_sst() {
809        let estimated_key_count =
810            estimate_output_key_count_by_size(1024 * 1024, 512 * 1024 * 1024, 128 * 1024 * 1024);
811
812        assert_eq!(estimated_key_count, 256 * 1024);
813        assert_eq!(estimate_output_key_count_by_size(100, 0, 0), 100);
814    }
815
816    #[tokio::test]
817    async fn test_optimize_by_copy_block_respects_layout_policy() {
818        let context = test_context().await;
819        let compact_task = test_compact_task(
820            PbSstableFilterLayout::Plain,
821            Some(1),
822            PbSstableFilterType::SstableFilterXor16,
823        );
824
825        assert!(!optimize_by_copy_block(&compact_task, &context));
826
827        let compact_task = test_compact_task(
828            PbSstableFilterLayout::Auto,
829            Some(1024),
830            PbSstableFilterType::SstableFilterXor16,
831        );
832
833        assert!(!optimize_by_copy_block(&compact_task, &context));
834    }
835
836    #[tokio::test]
837    async fn test_optimize_by_copy_block_supports_blocked_xor_filters() {
838        let context = test_context().await;
839        let compact_task = test_compact_task(
840            PbSstableFilterLayout::Blocked,
841            Some(1024),
842            PbSstableFilterType::SstableFilterXor16,
843        );
844
845        assert!(optimize_by_copy_block(&compact_task, &context));
846
847        let compact_task = test_compact_task(
848            PbSstableFilterLayout::Blocked,
849            Some(1024),
850            PbSstableFilterType::SstableFilterXor8,
851        );
852
853        assert!(optimize_by_copy_block(&compact_task, &context));
854    }
855}