| // Licensed to the Apache Software Foundation (ASF) under one or more | |
| // contributor license agreements. See the NOTICE file distributed with | |
| // this work for additional information regarding copyright ownership. | |
| // The ASF licenses this file to You under the Apache License, Version 2.0 | |
| // (the "License"); you may not use this file except in compliance with | |
| // the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| //! Defines the External shuffle repartition plan | |
| use std::{ | |
| any::Any, | |
| collections::{HashSet, vec_deque::VecDeque}, | |
| fmt::Formatter, | |
| io::{Cursor, Read, Write}, | |
| sync::{ | |
| Arc, Weak, | |
| atomic::{AtomicUsize, Ordering::SeqCst}, | |
| }, | |
| }; | |
| use arrow::{ | |
| array::ArrayRef, | |
| compute::{SortOptions, sort_to_indices}, | |
| datatypes::{DataType, Schema, SchemaRef}, | |
| record_batch::{RecordBatch, RecordBatchOptions}, | |
| row::{RowConverter, Rows, SortField}, | |
| }; | |
| use async_trait::async_trait; | |
| use auron_memmgr::{ | |
| MemConsumer, MemConsumerInfo, MemManager, | |
| spill::{Spill, SpillCompressedReader, SpillCompressedWriter, try_new_spill}, | |
| }; | |
| use bytesize::ByteSize; | |
| use datafusion::{ | |
| common::{DataFusionError, Result, Statistics, utils::proxy::VecAllocExt}, | |
| execution::context::TaskContext, | |
| physical_expr::{ | |
| EquivalenceProperties, PhysicalExprRef, PhysicalSortExpr, expressions::Column, | |
| }, | |
| physical_plan::{ | |
| DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, PlanProperties, | |
| SendableRecordBatchStream, | |
| execution_plan::{Boundedness, EmissionType}, | |
| metrics::{ExecutionPlanMetricsSet, MetricsSet}, | |
| }, | |
| }; | |
| use datafusion_ext_commons::{ | |
| algorithm::loser_tree::{ComparableForLoserTree, LoserTree}, | |
| arrow::{ | |
| array_size::BatchSize, | |
| selection::{BatchInterleaver, create_batch_interleaver, take_batch}, | |
| }, | |
| compute_suggested_batch_size_for_kway_merge, compute_suggested_batch_size_for_output, | |
| downcast_any, | |
| io::{read_len, read_one_batch, write_len, write_one_batch}, | |
| }; | |
| use futures::StreamExt; | |
| use itertools::Itertools; | |
| use once_cell::sync::OnceCell; | |
| use parking_lot::Mutex; | |
| use crate::common::{ | |
| execution_context::{ | |
| ExecutionContext, WrappedRecordBatchSender, WrappedRecordBatchWithKeyRowsSender, | |
| WrappedSenderTrait, | |
| }, | |
| key_rows_output::{RecordBatchWithKeyRows, SendableRecordBatchWithKeyRowsStream}, | |
| timer_helper::TimerHelper, | |
| }; | |
| // reserve memory for each spill | |
| // estimated size: bufread=64KB + lz4dec.src=64KB + lz4dec.dest=64KB | |
| const SPILL_OFFHEAP_MEM_COST: usize = 200000; | |
| // max number of sorted batches to merge | |
| const NUM_MAX_MERGING_BATCHES: usize = 32; | |
| #[derive(Debug)] | |
| pub struct SortExec { | |
| input: Arc<dyn ExecutionPlan>, | |
| exprs: Vec<PhysicalSortExpr>, | |
| limit: Option<usize>, | |
| offset: usize, | |
| metrics: ExecutionPlanMetricsSet, | |
| record_output: bool, | |
| props: OnceCell<Arc<PlanProperties>>, | |
| } | |
| impl SortExec { | |
| pub fn new( | |
| input: Arc<dyn ExecutionPlan>, | |
| exprs: Vec<PhysicalSortExpr>, | |
| limit: Option<usize>, | |
| offset: usize, | |
| ) -> Self { | |
| let metrics = ExecutionPlanMetricsSet::new(); | |
| Self { | |
| input, | |
| exprs, | |
| limit, | |
| offset, | |
| metrics, | |
| record_output: true, | |
| props: OnceCell::new(), | |
| } | |
| } | |
| pub fn sort_exprs(&self) -> &[PhysicalSortExpr] { | |
| &self.exprs | |
| } | |
| } | |
| pub fn create_default_ascending_sort_exec( | |
| input: Arc<dyn ExecutionPlan>, | |
| key_exprs: &[PhysicalExprRef], | |
| execution_plan_metrics: Option<ExecutionPlanMetricsSet>, | |
| record_output: bool, | |
| ) -> Arc<dyn ExecutionPlan> { | |
| // This helper has no limit or offset, so an empty ordering is a no-op. | |
| // Preserve zero-column batch row counts instead of passing them through | |
| // RowConverter without any columns. | |
| if key_exprs.is_empty() { | |
| return input; | |
| } | |
| let mut sort_exec = SortExec::new( | |
| input, | |
| key_exprs | |
| .iter() | |
| .map(|e| PhysicalSortExpr { | |
| expr: e.clone(), | |
| options: Default::default(), | |
| }) | |
| .collect(), | |
| None, | |
| 0, | |
| ); | |
| if let Some(execution_plan_metrics) = execution_plan_metrics { | |
| sort_exec.metrics = execution_plan_metrics; | |
| sort_exec.record_output = record_output; | |
| } | |
| Arc::new(sort_exec) | |
| } | |
| impl DisplayAs for SortExec { | |
| fn fmt_as(&self, _t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result { | |
| let exprs = self | |
| .exprs | |
| .iter() | |
| .map(|e| e.to_string()) | |
| .collect::<Vec<_>>() | |
| .join(", "); | |
| write!(f, "SortExec: {exprs}") | |
| } | |
| } | |
| impl ExecutionPlan for SortExec { | |
| fn name(&self) -> &str { | |
| "SortExec" | |
| } | |
| fn schema(&self) -> SchemaRef { | |
| self.input.schema() | |
| } | |
| fn properties(&self) -> &Arc<PlanProperties> { | |
| self.props.get_or_init(|| { | |
| Arc::new(PlanProperties::new( | |
| EquivalenceProperties::new(self.schema()), | |
| self.input.output_partitioning().clone(), | |
| EmissionType::Both, | |
| Boundedness::Bounded, | |
| )) | |
| }) | |
| } | |
| fn apply_expressions( | |
| &self, | |
| _f: &mut dyn FnMut( | |
| &Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| ) -> Result<datafusion::common::tree_node::TreeNodeRecursion>, | |
| ) -> Result<datafusion::common::tree_node::TreeNodeRecursion> { | |
| Ok(datafusion::common::tree_node::TreeNodeRecursion::Continue) | |
| } | |
| fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> { | |
| vec![&self.input] | |
| } | |
| fn with_new_children( | |
| self: Arc<Self>, | |
| children: Vec<Arc<dyn ExecutionPlan>>, | |
| ) -> Result<Arc<dyn ExecutionPlan>> { | |
| Ok(Arc::new(Self::new( | |
| children[0].clone(), | |
| self.exprs.clone(), | |
| self.limit, | |
| self.offset, | |
| ))) | |
| } | |
| fn execute( | |
| &self, | |
| partition: usize, | |
| context: Arc<TaskContext>, | |
| ) -> Result<SendableRecordBatchStream> { | |
| let projection: Vec<usize> = (0..self.schema().fields().len()).collect(); | |
| self.execute_projected_without_key_rows(partition, context, &projection) | |
| } | |
| fn metrics(&self) -> Option<MetricsSet> { | |
| Some(self.metrics.clone_inner()) | |
| } | |
| } | |
| impl SortExec { | |
| fn init_sorter( | |
| &self, | |
| exec_ctx: Arc<ExecutionContext>, | |
| projection: &[usize], | |
| ) -> Result<Arc<ExternalSorter>> { | |
| let prune_sort_keys_from_batch = Arc::new(PruneSortKeysFromBatch::try_new( | |
| self.input.schema(), | |
| projection, | |
| &self.exprs, | |
| )?); | |
| let sorter = Arc::new_cyclic(|weak| ExternalSorter { | |
| exec_ctx, | |
| mem_consumer_info: OnceCell::new(), | |
| weak: weak.clone(), | |
| prune_sort_keys_from_batch: prune_sort_keys_from_batch.clone(), | |
| skip: self.offset, | |
| limit: self.limit.unwrap_or(usize::MAX), | |
| record_output: self.record_output, | |
| in_mem_blocks: Default::default(), | |
| spills: Default::default(), | |
| num_total_rows: Default::default(), | |
| mem_total_size: Default::default(), | |
| }); | |
| MemManager::register_consumer(sorter.clone(), true); | |
| Ok(sorter) | |
| } | |
| /// Execute with projection without outputting key rows | |
| fn execute_projected_without_key_rows( | |
| &self, | |
| partition: usize, | |
| context: Arc<TaskContext>, | |
| projection: &[usize], | |
| ) -> Result<SendableRecordBatchStream> { | |
| let output_schema = Arc::new(self.schema().project(projection)?); | |
| let exec_ctx = | |
| ExecutionContext::new(context.clone(), partition, output_schema, &self.metrics); | |
| let sorter = self.init_sorter(exec_ctx.clone(), projection)?; | |
| let elapsed_compute = exec_ctx.baseline_metrics().elapsed_compute().clone(); | |
| let input = exec_ctx.execute_with_input_stats(&self.input)?; | |
| let mut coalesced = exec_ctx.coalesce_with_default_batch_size(input); | |
| // Implement proper key rows output by using the existing sorter with key rows | |
| // support | |
| let output = exec_ctx | |
| .clone() | |
| .output_with_sender("Sort", move |sender| async move { | |
| let _timer = elapsed_compute.timer(); | |
| sender.exclude_time(&elapsed_compute); | |
| while let Some(batch) = elapsed_compute | |
| .exclude_timer_async(coalesced.next()) | |
| .await | |
| .transpose()? | |
| { | |
| sorter.insert_batch(batch).await?; | |
| } | |
| sorter.output(sender).await?; | |
| Ok(()) | |
| }); | |
| Ok(exec_ctx.coalesce_with_default_batch_size(output)) | |
| } | |
| /// Execute with projection and output key rows for optimization | |
| pub fn execute_with_key_rows( | |
| &self, | |
| partition: usize, | |
| context: Arc<TaskContext>, | |
| ) -> Result<SendableRecordBatchWithKeyRowsStream> { | |
| let projection: Vec<usize> = (0..self.schema().fields().len()).collect(); | |
| self.execute_projected_with_key_rows(partition, context, &projection) | |
| } | |
| /// Execute with projection and output key rows for optimization | |
| pub fn execute_projected_with_key_rows( | |
| &self, | |
| partition: usize, | |
| context: Arc<TaskContext>, | |
| projection: &[usize], | |
| ) -> Result<SendableRecordBatchWithKeyRowsStream> { | |
| let output_schema = Arc::new(self.schema().project(projection)?); | |
| let exec_ctx = | |
| ExecutionContext::new(context.clone(), partition, output_schema, &self.metrics); | |
| let sorter = self.init_sorter(exec_ctx.clone(), projection)?; | |
| let elapsed_compute = exec_ctx.baseline_metrics().elapsed_compute().clone(); | |
| let input = exec_ctx.execute_with_input_stats(&self.input)?; | |
| let mut coalesced = exec_ctx.coalesce_with_default_batch_size(input); | |
| // Implement proper key rows output by using the existing sorter with key rows | |
| // support | |
| let output = exec_ctx.clone().output_with_sender_with_key_rows( | |
| "Sort", | |
| &self.exprs, | |
| move |sender| async move { | |
| let _timer = elapsed_compute.timer(); | |
| sender.exclude_time(&elapsed_compute); | |
| while let Some(batch) = elapsed_compute | |
| .exclude_timer_async(coalesced.next()) | |
| .await | |
| .transpose()? | |
| { | |
| sorter.insert_batch(batch).await?; | |
| } | |
| sorter.output(sender).await?; | |
| Ok(()) | |
| }, | |
| ); | |
| Ok(output) | |
| } | |
| } | |
| struct LevelSpill { | |
| block: SpillSortedBlock, | |
| level: usize, | |
| } | |
| struct ExternalSorter { | |
| exec_ctx: Arc<ExecutionContext>, | |
| mem_consumer_info: OnceCell<Weak<MemConsumerInfo>>, | |
| weak: Weak<Self>, | |
| prune_sort_keys_from_batch: Arc<PruneSortKeysFromBatch>, | |
| skip: usize, | |
| limit: usize, | |
| record_output: bool, | |
| in_mem_blocks: Arc<Mutex<Vec<InMemSortedBlock>>>, | |
| spills: Arc<Mutex<Vec<LevelSpill>>>, | |
| num_total_rows: AtomicUsize, | |
| mem_total_size: AtomicUsize, | |
| } | |
| impl ExternalSorter { | |
| fn sub_batch_size(&self) -> usize { | |
| compute_suggested_batch_size_for_kway_merge(self.mem_total_size(), self.num_total_rows()) | |
| } | |
| fn output_batch_size(&self) -> usize { | |
| compute_suggested_batch_size_for_output(self.mem_total_size(), self.num_total_rows()) | |
| } | |
| fn pruned_schema(&self) -> SchemaRef { | |
| self.prune_sort_keys_from_batch.pruned_schema() | |
| } | |
| } | |
| #[async_trait] | |
| impl MemConsumer for ExternalSorter { | |
| fn name(&self) -> &str { | |
| "ExternalSorter" | |
| } | |
| fn set_consumer_info(&self, consumer_info: Weak<MemConsumerInfo>) { | |
| self.mem_consumer_info | |
| .set(consumer_info) | |
| .expect("consumer info already set"); | |
| } | |
| fn get_consumer_info(&self) -> &Weak<MemConsumerInfo> { | |
| self.mem_consumer_info.get().expect("consumer info not set") | |
| } | |
| async fn spill(&self) -> Result<()> { | |
| let spills = self.spills.clone(); | |
| let blocks = std::mem::take(&mut *self.in_mem_blocks.lock()); | |
| let self_arc = self.to_arc(); | |
| tokio::task::spawn_blocking(move || { | |
| let mut spills = spills.lock(); | |
| let spill = try_new_spill(self_arc.exec_ctx.spill_metrics())?; | |
| let merged_block = merge_blocks::<_, SqueezeKeyCollector>( | |
| self_arc.clone(), | |
| blocks, | |
| SpillSortedBlockBuilder::new(self_arc.pruned_schema(), spill), | |
| )?; | |
| spills.push(LevelSpill { | |
| block: merged_block, | |
| level: 0, | |
| }); | |
| // merge if there are too many spills | |
| let mut levels = std::mem::take(&mut *spills) | |
| .into_iter() | |
| .sorted_unstable_by_key(|spill| spill.level) | |
| .chunk_by(|spill| spill.level) | |
| .into_iter() | |
| .map(|(_, spills)| { | |
| spills | |
| .into_iter() | |
| .map(|spill| spill.block) | |
| .collect::<Vec<_>>() | |
| }) | |
| .collect::<Vec<_>>(); | |
| levels.push(vec![]); // add a new level for possible merging target | |
| for level in 0..levels.len() { | |
| if levels[level].len() >= NUM_MAX_MERGING_BATCHES { | |
| let spill = try_new_spill(self_arc.exec_ctx.spill_metrics())?; | |
| let merged = merge_blocks::<_, SqueezeKeyCollector>( | |
| self_arc.clone(), | |
| std::mem::take(&mut levels[level]), | |
| SpillSortedBlockBuilder::new(self_arc.pruned_schema(), spill), | |
| )?; | |
| levels[level + 1].push(merged); | |
| } else { | |
| spills.extend( | |
| std::mem::take(&mut levels[level]) | |
| .into_iter() | |
| .map(|block| LevelSpill { block, level }), | |
| ); | |
| } | |
| } | |
| Ok::<_, DataFusionError>(()) | |
| }) | |
| .await | |
| .expect("tokio error")?; | |
| self.update_mem_used(0).await?; | |
| Ok(()) | |
| } | |
| } | |
| trait SortedBlock: Send { | |
| fn is_single_batch(&self) -> bool; | |
| fn next_batch(&mut self) -> Result<Option<RecordBatch>>; | |
| fn next_key(&mut self) -> Result<()>; | |
| fn cur_key(&self) -> &[u8]; | |
| fn is_equal_to_prev_key(&self) -> bool; | |
| fn mem_used(&self) -> usize; | |
| } | |
| struct InMemSortedBlock { | |
| sorted_keys: VecDeque<InMemRowsKeyCollector>, | |
| sorted_batches: VecDeque<RecordBatch>, | |
| mem_used: usize, | |
| cur_row_idx: usize, | |
| } | |
| impl SortedBlock for InMemSortedBlock { | |
| fn is_single_batch(&self) -> bool { | |
| self.sorted_batches.len() == 1 | |
| } | |
| fn next_batch(&mut self) -> Result<Option<RecordBatch>> { | |
| let batch = self.sorted_batches.pop_front(); | |
| if let Some(batch) = batch { | |
| self.mem_used -= batch.get_batch_mem_size(); | |
| self.mem_used -= self.sorted_keys[0].mem_size(); | |
| self.sorted_keys.pop_front().expect("missing key"); | |
| self.cur_row_idx = usize::MAX; | |
| Ok(Some(batch)) | |
| } else { | |
| Ok(None) | |
| } | |
| } | |
| fn next_key(&mut self) -> Result<()> { | |
| self.cur_row_idx += 1; | |
| Ok(()) | |
| } | |
| fn cur_key(&self) -> &[u8] { | |
| self.sorted_keys[0].key(self.cur_row_idx) | |
| } | |
| fn is_equal_to_prev_key(&self) -> bool { | |
| self.cur_row_idx > 0 && { | |
| let key0 = self.sorted_keys[0].key(self.cur_row_idx - 1); | |
| let key1 = self.sorted_keys[0].key(self.cur_row_idx); | |
| key0 == key1 | |
| } | |
| } | |
| fn mem_used(&self) -> usize { | |
| self.mem_used | |
| } | |
| } | |
| struct SpillSortedBlock { | |
| pruned_schema: SchemaRef, | |
| spill_reader: SpillCompressedReader<'static>, | |
| cur_key_reader: SortedKeysReader, | |
| _spill: Box<dyn Spill>, | |
| } | |
| impl SortedBlock for SpillSortedBlock { | |
| fn is_single_batch(&self) -> bool { | |
| false | |
| } | |
| fn next_batch(&mut self) -> Result<Option<RecordBatch>> { | |
| if let Some((num_rows, cols)) = read_one_batch(&mut self.spill_reader, &self.pruned_schema)? | |
| { | |
| let batch = RecordBatch::try_new_with_options( | |
| self.pruned_schema.clone(), | |
| cols, | |
| &RecordBatchOptions::new().with_row_count(Some(num_rows)), | |
| )?; | |
| Ok(Some(batch)) | |
| } else { | |
| Ok(None) | |
| } | |
| } | |
| fn next_key(&mut self) -> Result<()> { | |
| Ok(self.cur_key_reader.next_key(&mut self.spill_reader)?) | |
| } | |
| fn cur_key(&self) -> &[u8] { | |
| &self.cur_key_reader.cur_key | |
| } | |
| fn is_equal_to_prev_key(&self) -> bool { | |
| self.cur_key_reader.is_equal_to_prev | |
| } | |
| fn mem_used(&self) -> usize { | |
| 0 | |
| } | |
| } | |
| trait SortedBlockBuilder<B: SortedBlock, KS: KeyCollector> { | |
| fn add_batch_and_keys(&mut self, batch: RecordBatch, keys: KS) -> Result<()>; | |
| fn finish(self) -> Result<B>; | |
| } | |
| #[derive(Default)] | |
| struct InMemSortedBlockBuilder { | |
| sorted_keys: VecDeque<InMemRowsKeyCollector>, | |
| sorted_batches: VecDeque<RecordBatch>, | |
| mem_used: usize, | |
| } | |
| impl SortedBlockBuilder<InMemSortedBlock, InMemRowsKeyCollector> for InMemSortedBlockBuilder { | |
| fn add_batch_and_keys( | |
| &mut self, | |
| batch: RecordBatch, | |
| keys: InMemRowsKeyCollector, | |
| ) -> Result<()> { | |
| self.mem_used += batch.get_batch_mem_size(); | |
| self.mem_used += keys.mem_size(); | |
| self.sorted_keys.push_back(keys); | |
| self.sorted_batches.push_back(batch); | |
| Ok(()) | |
| } | |
| fn finish(mut self) -> Result<InMemSortedBlock> { | |
| self.sorted_keys | |
| .push_front(InMemRowsKeyCollector::default()); // for first pop | |
| Ok(InMemSortedBlock { | |
| sorted_keys: self.sorted_keys, | |
| sorted_batches: self.sorted_batches, | |
| cur_row_idx: 0, | |
| mem_used: self.mem_used, | |
| }) | |
| } | |
| } | |
| struct SpillSortedBlockBuilder { | |
| pruned_schema: SchemaRef, | |
| spill: Box<dyn Spill>, | |
| spill_writer: SpillCompressedWriter<'static>, | |
| } | |
| impl SpillSortedBlockBuilder { | |
| fn new(pruned_schema: SchemaRef, mut spill: Box<dyn Spill>) -> Self { | |
| let spill_writer = unsafe { | |
| // safety: bypass lifetime check, spill writer has the same lifetime as spill | |
| std::mem::transmute(spill.get_compressed_writer()) | |
| }; | |
| Self { | |
| pruned_schema, | |
| spill, | |
| spill_writer, | |
| } | |
| } | |
| } | |
| impl SortedBlockBuilder<SpillSortedBlock, SqueezeKeyCollector> for SpillSortedBlockBuilder { | |
| fn add_batch_and_keys(&mut self, batch: RecordBatch, keys: SqueezeKeyCollector) -> Result<()> { | |
| write_one_batch(batch.num_rows(), batch.columns(), &mut self.spill_writer)?; | |
| self.spill_writer.write_all(&keys.store)?; | |
| Ok(()) | |
| } | |
| fn finish(self) -> Result<SpillSortedBlock> { | |
| let spill = self.spill; | |
| self.spill_writer.finish()?; | |
| let spill_reader = unsafe { | |
| // safety: bypass lifetime check, spill reader has the same lifetime as spill | |
| std::mem::transmute(spill.get_compressed_reader()) | |
| }; | |
| Ok(SpillSortedBlock { | |
| pruned_schema: self.pruned_schema, | |
| spill_reader, | |
| cur_key_reader: SortedKeysReader::default(), | |
| _spill: spill, | |
| }) | |
| } | |
| } | |
| impl Drop for ExternalSorter { | |
| fn drop(&mut self) { | |
| MemManager::deregister_consumer(self); | |
| } | |
| } | |
| impl ExternalSorter { | |
| async fn insert_batch(self: &Arc<Self>, batch: RecordBatch) -> Result<()> { | |
| if batch.num_rows() == 0 { | |
| return Ok(()); | |
| } | |
| self.num_total_rows.fetch_add(batch.num_rows(), SeqCst); | |
| self.mem_total_size | |
| .fetch_add(batch.get_batch_mem_size(), SeqCst); | |
| // For a single-column primitive key (e.g. TPC-H `lineitem.l_orderkey`), | |
| // skip the RowConverter byte-comparison sort and use Arrow | |
| // `sort_to_indices` on the raw array instead (~6x faster on the batch | |
| // in-place sort). The batch is sorted and limited here; the rest of the | |
| // pipeline (encode for K-way merge, output) is unchanged. | |
| let is_fast_path = self | |
| .prune_sort_keys_from_batch | |
| .primitive_fast_path | |
| .is_some(); | |
| let batch = if is_fast_path { | |
| self.prune_sort_keys_from_batch | |
| .sort_batch_in_place(batch, self.limit)? | |
| } else { | |
| batch | |
| }; | |
| // NOTE: we use stable merge sort for longer keys due to less comparison | |
| let (keys, batch) = self.prune_sort_keys_from_batch.prune(batch)?; | |
| // On the fast path the batch is already sorted (and limited) above, so | |
| // the key rows from `prune` are in final order — we can append them | |
| // sequentially and skip allocating an identity `Vec<u32>`. The non-fast | |
| // path needs an actual sorted-index vector for both key appending and | |
| // the `take_batch` below. | |
| let sorted_indices = if is_fast_path { | |
| None | |
| } else if keys.size() / keys.num_rows() <= 8 { | |
| Some( | |
| (0..keys.num_rows() as u32) | |
| .sorted_unstable_by_key(|&row_idx| unsafe { | |
| // safety: bypass boundary and lifetime checking | |
| std::mem::transmute::<_, &'static [u8]>( | |
| keys.row_unchecked(row_idx as usize).as_ref(), | |
| ) | |
| }) | |
| .take(self.limit) | |
| .collect::<Vec<_>>(), | |
| ) | |
| } else { | |
| Some( | |
| (0..keys.num_rows() as u32) | |
| .sorted_by_key(|&row_idx| unsafe { | |
| // safety: bypass boundary and lifetime checking | |
| std::mem::transmute::<_, &'static [u8]>( | |
| keys.row_unchecked(row_idx as usize).as_ref(), | |
| ) | |
| }) | |
| .take(self.limit) | |
| .collect::<Vec<_>>(), | |
| ) | |
| }; | |
| // build keys | |
| let mut key_collector = InMemRowsKeyCollector::default(); | |
| key_collector.reserve(keys.num_rows(), keys.size()); | |
| match sorted_indices.as_deref() { | |
| Some(indices) => { | |
| for &row_idx in indices { | |
| key_collector.add_key(keys.row(row_idx as usize).as_ref()); | |
| } | |
| } | |
| // fast path: rows are already in final order, append sequentially. | |
| None => { | |
| for row_idx in 0..keys.num_rows() as u32 { | |
| key_collector.add_key(keys.row(row_idx as usize).as_ref()); | |
| } | |
| } | |
| } | |
| key_collector.freeze(); | |
| // build batch | |
| let sorted_batch = if !self.prune_sort_keys_from_batch.is_all_pruned() { | |
| if is_fast_path { | |
| // batch is already sorted and pruned by `sort_batch_in_place`; | |
| // avoid an extra `take_batch` copy. | |
| batch | |
| } else { | |
| take_batch(batch, sorted_indices.expect("non-fast-path indices"))? | |
| } | |
| } else { | |
| create_zero_column_batch(sorted_indices.as_ref().map_or(keys.num_rows(), Vec::len)) | |
| }; | |
| // add to in-mem blocks | |
| let mut block_builder = InMemSortedBlockBuilder::default(); | |
| block_builder.add_batch_and_keys(sorted_batch, key_collector)?; | |
| self.in_mem_blocks.lock().push(block_builder.finish()?); | |
| let mem_used = self.mem_used(); | |
| self.update_mem_used(mem_used).await?; | |
| Ok(()) | |
| } | |
| async fn output(self: &Arc<Self>, sender: Arc<impl WrappedSenderTrait>) -> Result<()> { | |
| // if external merging is required, we need to spill in-mem data | |
| // to free memory as soon as possible | |
| let has_spill = !self.spills.lock().is_empty(); | |
| let has_in_mem_data = !self.in_mem_blocks.lock().is_empty(); | |
| if has_spill && has_in_mem_data { | |
| log::info!( | |
| "{} spills rest in-mem data to disk, size={}", | |
| self.name(), | |
| ByteSize(self.mem_used() as u64), | |
| ); | |
| self.spill().await?; | |
| } | |
| self.set_spillable(false); | |
| let output_batch_size = self.output_batch_size(); | |
| let spills = self.spills.lock().drain(..).collect::<Vec<_>>(); | |
| log::info!( | |
| "{} starts outputting ({} spills + in_mem: {})", | |
| self.name(), | |
| spills.len(), | |
| ByteSize(self.mem_used() as u64) | |
| ); | |
| // no spills -- output in-mem batches | |
| if spills.is_empty() { | |
| let in_mem_blocks = std::mem::take(&mut *self.in_mem_blocks.lock()); | |
| if !in_mem_blocks.is_empty() { | |
| let mut merger = Merger::try_new(self.clone(), in_mem_blocks)?; | |
| if self.skip > 0 { | |
| let _ = merger.skip_rows::<InMemRowsKeyCollector>(self.skip, output_batch_size); | |
| } | |
| while let Some((key_collector, pruned_batch)) = | |
| merger.next::<InMemRowsKeyCollector>(output_batch_size)? | |
| { | |
| if self.record_output { | |
| self.exec_ctx | |
| .baseline_metrics() | |
| .record_output(pruned_batch.num_rows()); | |
| } | |
| send_output_batch( | |
| sender.as_ref(), | |
| pruned_batch, | |
| &self.prune_sort_keys_from_batch, | |
| key_collector, | |
| ) | |
| .await?; | |
| } | |
| } | |
| self.update_mem_used(0).await?; | |
| return Ok(()); | |
| } | |
| let spill_blocks = spills.into_iter().map(|spill| spill.block).collect(); | |
| let mut merger = Merger::try_new(self.to_arc(), spill_blocks)?; | |
| if self.skip > 0 { | |
| let _ = merger.skip_rows::<InMemRowsKeyCollector>(self.skip, output_batch_size); | |
| } | |
| while let Some((key_collector, pruned_batch)) = | |
| merger.next::<InMemRowsKeyCollector>(output_batch_size)? | |
| { | |
| self.update_mem_used(merger.cursors_mem_used()).await?; | |
| if self.record_output { | |
| self.exec_ctx | |
| .baseline_metrics() | |
| .record_output(pruned_batch.num_rows()); | |
| } | |
| send_output_batch( | |
| sender.as_ref(), | |
| pruned_batch, | |
| &self.prune_sort_keys_from_batch, | |
| key_collector, | |
| ) | |
| .await?; | |
| } | |
| self.update_mem_used(0).await?; | |
| Ok(()) | |
| } | |
| fn to_arc(&self) -> Arc<Self> { | |
| self.weak.upgrade().expect("ExternalSorter.weak not set") | |
| } | |
| fn num_total_rows(&self) -> usize { | |
| self.num_total_rows.load(SeqCst) | |
| } | |
| fn mem_total_size(&self) -> usize { | |
| self.mem_total_size.load(SeqCst) | |
| } | |
| fn mem_used(&self) -> usize { | |
| let in_mem_blocks = self.in_mem_blocks.lock(); | |
| in_mem_blocks | |
| .iter() | |
| .map(|block| block.mem_used()) | |
| .sum::<usize>() | |
| } | |
| } | |
| async fn send_output_batch( | |
| sender: &impl WrappedSenderTrait, | |
| pruned_batch: RecordBatch, | |
| prune_sort_keys_from_batch: &PruneSortKeysFromBatch, | |
| key_collector: impl KeyCollector, | |
| ) -> Result<()> { | |
| if let Ok(sender) = downcast_any!(sender, WrappedRecordBatchSender) { | |
| let batch = prune_sort_keys_from_batch.restore(pruned_batch, key_collector)?; | |
| sender.send(batch).await | |
| } else if let Ok(sender) = downcast_any!(sender, WrappedRecordBatchWithKeyRowsSender) { | |
| let key_rows = Arc::new(key_collector.into_rows( | |
| pruned_batch.num_rows(), | |
| &prune_sort_keys_from_batch.sort_row_converter.lock(), | |
| )?); | |
| let batch = | |
| prune_sort_keys_from_batch.restore_from_existed_key_rows(pruned_batch, &key_rows)?; | |
| sender | |
| .send(RecordBatchWithKeyRows::new( | |
| batch, | |
| key_rows, | |
| prune_sort_keys_from_batch.sort_row_converter.clone(), | |
| )) | |
| .await; | |
| } | |
| Ok(()) | |
| } | |
| struct SortedBlockCursor<B: SortedBlock> { | |
| id: usize, | |
| input: B, | |
| cur_batch_num_rows: usize, | |
| cur_batches: Vec<RecordBatch>, | |
| cur_batches_changed: bool, | |
| cur_key_row_idx: usize, | |
| cur_batch_idx: usize, | |
| cur_row_idx: usize, | |
| cur_mem_used: usize, | |
| finished: bool, | |
| } | |
| impl<B: SortedBlock> ComparableForLoserTree for SortedBlockCursor<B> { | |
| #[inline(always)] | |
| fn lt(&self, other: &Self) -> bool { | |
| if other.finished || self.finished { | |
| return other.finished; | |
| } | |
| self.input.cur_key() < other.input.cur_key() | |
| } | |
| } | |
| impl<B: SortedBlock> SortedBlockCursor<B> { | |
| fn try_from_block(id: usize, block: B) -> Result<Self> { | |
| let mut new = Self { | |
| id, | |
| input: block, | |
| cur_batch_num_rows: 0, | |
| cur_batches: vec![], | |
| cur_batches_changed: false, | |
| cur_key_row_idx: 0, | |
| cur_batch_idx: 0, | |
| cur_row_idx: 0, | |
| cur_mem_used: 0, | |
| finished: false, | |
| }; | |
| new.next_key()?; // load first record | |
| Ok(new) | |
| } | |
| fn block(&self) -> &B { | |
| &self.input | |
| } | |
| // forwards to next key and returns current key | |
| fn next_key(&mut self) -> Result<()> { | |
| assert!( | |
| !self.finished, | |
| "calling next_key() on finished sort spill cursor" | |
| ); | |
| if self.cur_key_row_idx >= self.cur_batches.last().map(|b| b.num_rows()).unwrap_or(0) | |
| && !self.load_next_batch()? | |
| { | |
| self.finished = true; | |
| return Ok(()); | |
| } | |
| self.input.next_key()?; | |
| self.cur_key_row_idx += 1; | |
| Ok(()) | |
| } | |
| fn load_next_batch(&mut self) -> Result<bool> { | |
| if let Some(batch) = self.input.next_batch()? { | |
| self.cur_batch_num_rows = batch.num_rows(); | |
| self.cur_mem_used += batch.get_batch_mem_size(); | |
| self.cur_batches.push(batch); | |
| self.cur_key_row_idx = 0; | |
| self.cur_batches_changed = true; | |
| return Ok(true); | |
| } | |
| self.finished = true; | |
| Ok(false) | |
| } | |
| fn next_row(&mut self) -> (usize, usize) { | |
| let batch_idx = self.cur_batch_idx; | |
| let row_idx = self.cur_row_idx; | |
| self.cur_row_idx += 1; | |
| if self.cur_row_idx >= self.cur_batches[self.cur_batch_idx].num_rows() { | |
| self.cur_batch_idx += 1; | |
| self.cur_row_idx = 0; | |
| } | |
| (batch_idx, row_idx) | |
| } | |
| fn clear_finished_batches(&mut self) { | |
| if self.cur_batch_idx > 0 { | |
| for batch in self.cur_batches.drain(..self.cur_batch_idx) { | |
| self.cur_batches_changed = true; | |
| self.cur_mem_used -= batch.get_batch_mem_size(); | |
| } | |
| self.cur_batch_idx = 0; | |
| } | |
| } | |
| } | |
| struct Merger<B: SortedBlock> { | |
| sorter: Arc<ExternalSorter>, | |
| cursors: LoserTree<SortedBlockCursor<B>>, | |
| batch_interleaver: BatchInterleaver, | |
| single_batch_mode: bool, | |
| batches_base_indices: Vec<usize>, | |
| num_total_output_rows: usize, | |
| } | |
| impl<B: SortedBlock> Merger<B> { | |
| fn try_new(sorter: Arc<ExternalSorter>, blocks: Vec<B>) -> Result<Self> { | |
| let single_batch_mode = blocks.iter().all(|block| block.is_single_batch()); | |
| let cursors = LoserTree::new( | |
| blocks | |
| .into_iter() | |
| .enumerate() | |
| .map(|(id, block)| SortedBlockCursor::try_from_block(id, block)) | |
| .collect::<Result<Vec<_>>>()?, | |
| ); | |
| let batch_interleaver = if single_batch_mode { | |
| let batches = cursors | |
| .values() | |
| .iter() | |
| .map(|cursor| cursor.cur_batches[0].clone()) | |
| .collect::<Vec<_>>(); | |
| create_batch_interleaver(&batches, true)? | |
| } else { | |
| create_batch_interleaver(&[RecordBatch::new_empty(sorter.pruned_schema())], true)? | |
| }; | |
| Ok(Self { | |
| sorter, | |
| cursors, | |
| batch_interleaver, | |
| single_batch_mode, | |
| batches_base_indices: vec![], | |
| num_total_output_rows: 0, | |
| }) | |
| } | |
| fn cursors_mem_used(&self) -> usize { | |
| self.cursors.len() * SPILL_OFFHEAP_MEM_COST | |
| + self | |
| .cursors | |
| .values() | |
| .iter() | |
| .map(|cursor| cursor.cur_mem_used) | |
| .sum::<usize>() | |
| } | |
| fn next<KC: KeyCollector>(&mut self, batch_size: usize) -> Result<Option<(KC, RecordBatch)>> { | |
| let max_num_rows = batch_size.min(self.sorter.limit - self.num_total_output_rows); | |
| let mut num_rows = 0; | |
| // collect merged records to staging | |
| let mut min_cursor = self.cursors.peek_mut(); | |
| let mut key_collector = KC::default(); | |
| let mut cursor_ids = vec![]; | |
| key_collector.reserve(num_rows, 0); | |
| while !min_cursor.finished && num_rows < max_num_rows { | |
| if !self.sorter.prune_sort_keys_from_batch.is_all_pruned() { | |
| cursor_ids.push(min_cursor.id); | |
| } | |
| key_collector.add_key(&min_cursor.block().cur_key()); | |
| min_cursor.next_key()?; | |
| num_rows += 1; | |
| // fetch next min key from loser tree only if it is different from previous key | |
| if min_cursor.finished || !min_cursor.block().is_equal_to_prev_key() { | |
| min_cursor.adjust(); | |
| } | |
| } | |
| if num_rows == 0 { | |
| return Ok(None); | |
| } | |
| key_collector.freeze(); | |
| drop(min_cursor); | |
| // collect pruned columns | |
| let pruned_batch = if !self.sorter.prune_sort_keys_from_batch.is_all_pruned() { | |
| if !self.single_batch_mode | |
| && self | |
| .cursors | |
| .values() | |
| .iter() | |
| .any(|cursor| cursor.cur_batches_changed) | |
| { | |
| let mut batches_base_indices = vec![]; | |
| let mut batches = vec![]; | |
| for cursor in self.cursors.values_mut() { | |
| batches_base_indices.push(batches.len()); | |
| batches.extend(cursor.cur_batches.clone()); | |
| cursor.cur_batches_changed = false; | |
| } | |
| self.batch_interleaver = create_batch_interleaver(&batches, true)?; | |
| self.batches_base_indices = batches_base_indices; | |
| } | |
| let batch_interleaver = &self.batch_interleaver; | |
| // interleave output batch | |
| let staging_indices = if !self.single_batch_mode { | |
| cursor_ids | |
| .into_iter() | |
| .map(|cursor_id| { | |
| let cursor = &mut self.cursors.values_mut()[cursor_id]; | |
| let (batch_idx, row_idx) = cursor.next_row(); | |
| (self.batches_base_indices[cursor.id] + batch_idx, row_idx) | |
| }) | |
| .collect::<Vec<_>>() | |
| } else { | |
| cursor_ids | |
| .into_iter() | |
| .map(|cursor_id| { | |
| let cursor = &mut self.cursors.values_mut()[cursor_id]; | |
| let (_, row_idx) = cursor.next_row(); | |
| (cursor.id, row_idx) | |
| }) | |
| .collect::<Vec<_>>() | |
| }; | |
| batch_interleaver(&staging_indices)? | |
| } else { | |
| create_zero_column_batch(num_rows) | |
| }; | |
| self.num_total_output_rows += num_rows; | |
| for cursor in self.cursors.values_mut() { | |
| cursor.clear_finished_batches(); | |
| } | |
| Ok(Some((key_collector, pruned_batch))) | |
| } | |
| pub fn skip_rows<KC: KeyCollector>( | |
| &mut self, | |
| skip: usize, | |
| suggested_batch_size: usize, | |
| ) -> Result<()> { | |
| let mut remaining = skip; | |
| while remaining > 0 { | |
| let batch_size = remaining.min(suggested_batch_size); | |
| if self.next::<KC>(batch_size)?.is_none() { | |
| break; | |
| } | |
| remaining -= batch_size; | |
| } | |
| Ok(()) | |
| } | |
| } | |
| fn merge_blocks<B: SortedBlock, KC: KeyCollector>( | |
| sorter: Arc<ExternalSorter>, | |
| blocks: Vec<impl SortedBlock>, | |
| mut block_builder: impl SortedBlockBuilder<B, KC>, | |
| ) -> Result<B> { | |
| assert!(blocks.len() >= 1); | |
| let sub_batch_size = sorter.sub_batch_size(); | |
| let mut merger = Merger::try_new(sorter, blocks)?; | |
| while let Some((key_collector, pruned_batch)) = merger.next::<KC>(sub_batch_size)? { | |
| block_builder.add_batch_and_keys(pruned_batch, key_collector)?; | |
| } | |
| Ok(block_builder.finish()?) | |
| } | |
| fn create_zero_column_batch(num_rows: usize) -> RecordBatch { | |
| static EMPTY_SCHEMA: OnceCell<SchemaRef> = OnceCell::new(); | |
| let empty_schema = EMPTY_SCHEMA | |
| .get_or_init(|| Arc::new(Schema::empty())) | |
| .clone(); | |
| RecordBatch::try_new_with_options( | |
| empty_schema, | |
| vec![], | |
| &RecordBatchOptions::new().with_row_count(Some(num_rows)), | |
| ) | |
| .expect("failed to create empty RecordBatch") | |
| } | |
| struct PrimitiveSortKey { | |
| sort_expr: PhysicalSortExpr, | |
| } | |
| impl PrimitiveSortKey { | |
| fn try_new(input_schema: &SchemaRef, exprs: &[PhysicalSortExpr]) -> Option<Self> { | |
| if exprs.len() != 1 { | |
| return None; | |
| } | |
| let expr = &exprs[0]; | |
| // must be a direct column reference | |
| expr.expr.downcast_ref::<Column>()?; | |
| let data_type = expr.expr.data_type(input_schema).ok()?; | |
| if !matches!( | |
| data_type, | |
| DataType::Int8 | |
| | DataType::Int16 | |
| | DataType::Int32 | |
| | DataType::Int64 | |
| | DataType::UInt8 | |
| | DataType::UInt16 | |
| | DataType::UInt32 | |
| | DataType::UInt64 | |
| | DataType::Date32 | |
| | DataType::Date64 | |
| | DataType::Timestamp(_, _) | |
| ) { | |
| return None; | |
| } | |
| Some(Self { | |
| sort_expr: expr.clone(), | |
| }) | |
| } | |
| } | |
| struct PruneSortKeysFromBatch { | |
| input_projection: Vec<usize>, | |
| sort_row_converter: Arc<Mutex<RowConverter>>, | |
| key_exprs: Vec<PhysicalSortExpr>, | |
| key_cols: HashSet<usize>, | |
| restored_col_mappers: Vec<ColMapper>, | |
| restored_schema: SchemaRef, | |
| pruned_schema: SchemaRef, | |
| primitive_fast_path: Option<PrimitiveSortKey>, | |
| } | |
| #[derive(Clone, Copy)] | |
| enum ColMapper { | |
| FromPrunedBatch(usize), | |
| FromKey(usize), | |
| } | |
| impl PruneSortKeysFromBatch { | |
| fn try_new( | |
| input_schema: SchemaRef, | |
| input_projection: &[usize], | |
| exprs: &[PhysicalSortExpr], | |
| ) -> Result<Self> { | |
| let sort_row_converter = Arc::new(Mutex::new(RowConverter::new( | |
| exprs | |
| .iter() | |
| .map(|expr: &PhysicalSortExpr| { | |
| Ok(SortField::new_with_options( | |
| expr.expr.data_type(&input_schema)?, | |
| expr.options, | |
| )) | |
| }) | |
| .collect::<Result<Vec<SortField>>>()?, | |
| )?)); | |
| let input_projected_schema = Arc::new(input_schema.project(input_projection)?); | |
| let mut relation = vec![]; | |
| for (expr_idx, expr) in exprs.iter().enumerate() { | |
| if let Some(col) = expr.expr.downcast_ref::<Column>() { | |
| // Relation indices are consumed against the projected schema below. | |
| relation.extend(input_projection.iter().enumerate().filter_map( | |
| |(projected_idx, &input_idx)| { | |
| (input_idx == col.index()).then_some((expr_idx, projected_idx)) | |
| }, | |
| )); | |
| } | |
| } | |
| // compute pruned col indices | |
| let pruned_cols = relation | |
| .iter() | |
| .map(|(_, col_idx)| *col_idx) | |
| .collect::<HashSet<_>>(); | |
| // compute schema after pruning | |
| let mut fields_after_pruning = vec![]; | |
| for field_idx in 0..input_projected_schema.fields().len() { | |
| if !pruned_cols.contains(&field_idx) { | |
| fields_after_pruning.push(input_projected_schema.field(field_idx).clone()); | |
| } | |
| } | |
| let pruned_schema = Arc::new(Schema::new(fields_after_pruning)); | |
| // compute col mappers for restoring | |
| let restored_schema = input_projected_schema; | |
| let mut restored_col_mappers = vec![]; | |
| let mut num_pruned_cols = 0; | |
| for col_idx in 0..restored_schema.fields().len() { | |
| if let Some(expr_idx) = relation.iter().find(|kv| kv.1 == col_idx).map(|kv| kv.0) { | |
| restored_col_mappers.push(ColMapper::FromKey(expr_idx)); | |
| num_pruned_cols += 1; | |
| } else { | |
| restored_col_mappers.push(ColMapper::FromPrunedBatch(col_idx - num_pruned_cols)); | |
| } | |
| } | |
| Ok(Self { | |
| input_projection: input_projection.to_vec(), | |
| sort_row_converter, | |
| key_exprs: exprs.to_vec(), | |
| key_cols: pruned_cols, | |
| restored_col_mappers, | |
| pruned_schema, | |
| restored_schema, | |
| primitive_fast_path: PrimitiveSortKey::try_new(&input_schema, exprs), | |
| }) | |
| } | |
| fn is_all_pruned(&self) -> bool { | |
| self.pruned_schema.fields().is_empty() | |
| } | |
| /// Fast-path batch in-place sort for a single-column primitive key. | |
| /// | |
| /// Sorts the batch by the primitive sort key using Arrow `sort_to_indices` | |
| /// (which compares the raw values, e.g. `i64`, instead of `RowConverter` | |
| /// byte encoding), then reorders the batch by the resulting indices and | |
| /// applies `limit` rows. The caller can then treat the returned batch as | |
| /// already sorted and pruned. No-op precondition: `primitive_fast_path` | |
| /// must be `Some`; the caller checks `is_fast_path` before calling. | |
| fn sort_batch_in_place(&self, batch: RecordBatch, limit: usize) -> Result<RecordBatch> { | |
| // safety/correctness: only reached when is_fast_path == true | |
| let key = self | |
| .primitive_fast_path | |
| .as_ref() | |
| .expect("sort_batch_in_place requires a primitive fast path"); | |
| let array = key | |
| .sort_expr | |
| .expr | |
| .evaluate(&batch) | |
| .and_then(|cv| cv.into_array(batch.num_rows()))?; | |
| let options = SortOptions { | |
| descending: key.sort_expr.options.descending, | |
| nulls_first: key.sort_expr.options.nulls_first, | |
| }; | |
| // Pass `limit` straight to `sort_to_indices` so Arrow uses partial | |
| // sorting (it only emits the top-`limit` indices); when `limit` | |
| // covers the whole batch Arrow internally falls back to a full sort, | |
| // so this is never worse than `None`. The returned `UInt32Array` is | |
| // handed directly to `take_batch` (which accepts a `PrimitiveArray`) | |
| // instead of being copied through an intermediate `Vec<u32>`. | |
| let indices = sort_to_indices(&array, Some(options), Some(limit))?; | |
| take_batch(batch, indices) | |
| } | |
| fn pruned_schema(&self) -> SchemaRef { | |
| self.pruned_schema.clone() | |
| } | |
| fn restored_schema(&self) -> SchemaRef { | |
| self.restored_schema.clone() | |
| } | |
| fn prune(&self, batch: RecordBatch) -> Result<(Rows, RecordBatch)> { | |
| // compute key rows | |
| let key_cols: Vec<ArrayRef> = self | |
| .key_exprs | |
| .iter() | |
| .map(|expr| { | |
| expr.expr | |
| .evaluate(&batch) | |
| .and_then(|cv| cv.into_array(batch.num_rows())) | |
| }) | |
| .collect::<Result<_>>()?; | |
| let key_rows = self.sort_row_converter.lock().convert_columns(&key_cols)?; | |
| let retained_cols = batch | |
| .project(&self.input_projection)? | |
| .columns() | |
| .iter() | |
| .enumerate() | |
| .filter(|(col_idx, _)| !self.key_cols.contains(col_idx)) | |
| .map(|(_, col)| col) | |
| .cloned() | |
| .collect(); | |
| let pruned_batch = RecordBatch::try_new_with_options( | |
| self.pruned_schema(), | |
| retained_cols, | |
| &RecordBatchOptions::new().with_row_count(Some(batch.num_rows())), | |
| )?; | |
| Ok((key_rows, pruned_batch)) | |
| } | |
| fn restore<'a, KC: KeyCollector>( | |
| &self, | |
| pruned_batch: RecordBatch, | |
| key_collector: KC, | |
| ) -> Result<RecordBatch> { | |
| let num_rows = pruned_batch.num_rows(); | |
| let key_rows = key_collector.into_rows(num_rows, &self.sort_row_converter.lock())?; | |
| self.restore_from_existed_key_rows(pruned_batch, &key_rows) | |
| } | |
| fn restore_from_existed_key_rows( | |
| &self, | |
| pruned_batch: RecordBatch, | |
| key_rows: &Rows, | |
| ) -> Result<RecordBatch> { | |
| let key_cols = OnceCell::new(); | |
| // restore batch | |
| let mut restored_fields = vec![]; | |
| for &map in &self.restored_col_mappers { | |
| match map { | |
| ColMapper::FromPrunedBatch(idx) => { | |
| restored_fields.push(pruned_batch.column(idx).clone()); | |
| } | |
| ColMapper::FromKey(idx) => { | |
| let sort_row_converter = self.sort_row_converter.clone(); | |
| let key_cols = key_cols.get_or_try_init(move || { | |
| sort_row_converter.lock().convert_rows(key_rows.iter()) | |
| })?; | |
| restored_fields.push(key_cols[idx].clone()); | |
| } | |
| } | |
| } | |
| Ok(RecordBatch::try_new_with_options( | |
| self.restored_schema(), | |
| restored_fields, | |
| &RecordBatchOptions::new().with_row_count(Some(pruned_batch.num_rows())), | |
| )?) | |
| } | |
| } | |
| trait KeyCollector: Default { | |
| fn reserve(&mut self, num_rows: usize, data_size: usize); | |
| fn add_key(&mut self, key: &[u8]); | |
| fn freeze(&mut self); | |
| fn mem_size(&self) -> usize; | |
| fn into_rows(self, num_rows: usize, row_converter: &RowConverter) -> Result<Rows>; | |
| } | |
| #[derive(Default, Clone)] | |
| struct InMemRowsKeyCollector { | |
| buffer: Vec<u8>, | |
| offsets: Vec<usize>, | |
| } | |
| impl InMemRowsKeyCollector { | |
| fn key(&self, i: usize) -> &[u8] { | |
| unsafe { | |
| // safety: performance critical, bypass bounds check | |
| let start = *self.offsets.get_unchecked(i); | |
| let end = *self.offsets.get_unchecked(i + 1); | |
| self.buffer.get_unchecked(start..end) | |
| } | |
| } | |
| } | |
| impl KeyCollector for InMemRowsKeyCollector { | |
| fn reserve(&mut self, num_rows: usize, data_size: usize) { | |
| self.offsets.reserve(num_rows + 1); | |
| self.buffer.reserve(data_size); | |
| } | |
| fn add_key(&mut self, key: &[u8]) { | |
| self.offsets.push(self.buffer.len()); | |
| self.buffer.extend_from_slice(key); | |
| } | |
| fn freeze(&mut self) { | |
| self.offsets.push(self.buffer.len()); | |
| self.offsets.shrink_to_fit(); | |
| self.buffer.shrink_to_fit(); | |
| } | |
| fn mem_size(&self) -> usize { | |
| self.offsets.allocated_size() + self.buffer.allocated_size() | |
| } | |
| fn into_rows(self, num_rows: usize, row_converter: &RowConverter) -> Result<Rows> { | |
| assert_eq!(self.offsets.len() - 1, num_rows); | |
| let mut rows = row_converter.empty_rows(0, 0); | |
| unsafe { | |
| // safety: access rows.buffer/offsets | |
| struct XRows { | |
| buffer: Vec<u8>, | |
| offsets: Vec<usize>, | |
| } | |
| let xrows = std::mem::transmute::<_, &mut XRows>(&mut rows); | |
| xrows.buffer = self.buffer; | |
| xrows.offsets = self.offsets; | |
| } | |
| Ok(rows) | |
| } | |
| } | |
| #[derive(Default)] | |
| struct SqueezeKeyCollector { | |
| sorted_key_writer: SortedKeysWriter, | |
| store: Vec<u8>, | |
| } | |
| impl KeyCollector for SqueezeKeyCollector { | |
| fn reserve(&mut self, _num_rows: usize, _data_size: usize) { | |
| // do nothing because we cannot get squeezed data size at this moment | |
| } | |
| fn add_key(&mut self, key: &[u8]) { | |
| self.sorted_key_writer | |
| .write_key(key, &mut self.store) | |
| .expect("failed to write key"); | |
| } | |
| fn freeze(&mut self) { | |
| self.store.shrink_to_fit(); | |
| } | |
| fn mem_size(&self) -> usize { | |
| self.store.allocated_size() | |
| } | |
| fn into_rows(self, num_rows: usize, row_converter: &RowConverter) -> Result<Rows> { | |
| let mut sorted_key_reader = SortedKeysReader::default(); | |
| let mut r = Cursor::new(self.store); | |
| let mut simple_key_collector = InMemRowsKeyCollector::default(); | |
| for _ in 0..num_rows { | |
| sorted_key_reader.next_key(&mut r)?; | |
| simple_key_collector.add_key(&sorted_key_reader.cur_key); | |
| } | |
| simple_key_collector.into_rows(num_rows, row_converter) | |
| } | |
| } | |
| #[derive(Default)] | |
| struct SortedKeysWriter { | |
| cur_key: Vec<u8>, | |
| } | |
| impl SortedKeysWriter { | |
| fn write_key(&mut self, key: &[u8], w: &mut impl Write) -> std::io::Result<()> { | |
| let prefix_len = common_prefix_len(&self.cur_key, key); | |
| let suffix_len = key.len() - prefix_len; | |
| if prefix_len == key.len() && suffix_len == 0 { | |
| write_len(0, w)?; // indicates same record | |
| } else { | |
| let write_buf = unsafe { | |
| // safety: bypass bounds check in performance critical code | |
| self.cur_key | |
| .reserve(key.len().saturating_sub(self.cur_key.len())); | |
| self.cur_key.set_len(key.len()); | |
| let write_buf = self.cur_key.get_unchecked_mut(prefix_len..key.len()); | |
| write_buf.copy_from_slice(key.get_unchecked(prefix_len..)); | |
| write_buf | |
| }; | |
| write_len(suffix_len + 1, w)?; | |
| write_len(prefix_len, w)?; | |
| w.write_all(write_buf)?; | |
| } | |
| Ok(()) | |
| } | |
| } | |
| #[derive(Default)] | |
| struct SortedKeysReader { | |
| cur_key: Vec<u8>, | |
| is_equal_to_prev: bool, | |
| } | |
| impl SortedKeysReader { | |
| fn next_key(&mut self, r: &mut impl Read) -> std::io::Result<()> { | |
| let b = read_len(r)?; | |
| if b > 0 { | |
| self.is_equal_to_prev = false; | |
| let suffix_len = b - 1; | |
| let prefix_len = read_len(r)?; | |
| let read_buf = unsafe { | |
| // safety: bypass bounds check in performance critical code | |
| let new_key_len = prefix_len + suffix_len; | |
| self.cur_key | |
| .reserve(new_key_len.saturating_sub(self.cur_key.len())); | |
| self.cur_key.set_len(new_key_len); | |
| self.cur_key.get_unchecked_mut(prefix_len..new_key_len) | |
| }; | |
| r.read_exact(read_buf)?; | |
| } else { | |
| self.is_equal_to_prev = true; | |
| } | |
| Ok(()) | |
| } | |
| } | |
| fn common_prefix_len(a: &[u8], b: &[u8]) -> usize { | |
| let min_len = a.len().min(b.len()); | |
| let mut lcp = 0; | |
| macro_rules! pread { | |
| ($v:expr, $off:expr, $nb:expr) => {{ | |
| // safety: bypass bounds check in performance critical code | |
| unsafe { std::ptr::read_unaligned($v.as_ptr().add($off) as *const [u8; $nb]) } | |
| }}; | |
| } | |
| while lcp + 8 <= min_len && pread!(a, lcp, 8) == pread!(b, lcp, 8) { | |
| lcp += 8; | |
| } | |
| lcp += (lcp + 4 <= min_len && pread!(a, lcp, 4) == pread!(b, lcp, 4)) as usize * 4; | |
| lcp += (lcp + 2 <= min_len && pread!(a, lcp, 2) == pread!(b, lcp, 2)) as usize * 2; | |
| lcp += (lcp + 1 <= min_len && pread!(a, lcp, 1) == pread!(b, lcp, 1)) as usize; | |
| lcp | |
| } | |
| #[cfg(test)] | |
| mod test { | |
| use std::sync::Arc; | |
| use arrow::{ | |
| array::Int32Array, | |
| compute::SortOptions, | |
| datatypes::{DataType, Field, Schema}, | |
| record_batch::RecordBatch, | |
| }; | |
| use auron_memmgr::MemManager; | |
| use datafusion::{ | |
| assert_batches_eq, | |
| common::Result, | |
| physical_expr::{PhysicalSortExpr, expressions::Column}, | |
| physical_plan::{ExecutionPlan, common, test::TestMemoryExec}, | |
| prelude::SessionContext, | |
| }; | |
| use futures::TryStreamExt; | |
| use super::common_prefix_len; | |
| use crate::sort_exec::SortExec; | |
| fn build_table_i32( | |
| a: (&str, &Vec<i32>), | |
| b: (&str, &Vec<i32>), | |
| c: (&str, &Vec<i32>), | |
| ) -> Result<RecordBatch> { | |
| let schema = Schema::new(vec![ | |
| Field::new(a.0, DataType::Int32, false), | |
| Field::new(b.0, DataType::Int32, false), | |
| Field::new(c.0, DataType::Int32, false), | |
| ]); | |
| let batch = RecordBatch::try_new( | |
| Arc::new(schema), | |
| vec![ | |
| Arc::new(Int32Array::from(a.1.clone())), | |
| Arc::new(Int32Array::from(b.1.clone())), | |
| Arc::new(Int32Array::from(c.1.clone())), | |
| ], | |
| )?; | |
| Ok(batch) | |
| } | |
| fn build_table( | |
| a: (&str, &Vec<i32>), | |
| b: (&str, &Vec<i32>), | |
| c: (&str, &Vec<i32>), | |
| ) -> Result<Arc<dyn ExecutionPlan>> { | |
| let batch = build_table_i32(a, b, c)?; | |
| let schema = batch.schema(); | |
| Ok(Arc::new(TestMemoryExec::try_new( | |
| &[vec![batch]], | |
| schema, | |
| None, | |
| )?)) | |
| } | |
| fn build_single_column_table(name: &str, values: Vec<i32>) -> Result<Arc<dyn ExecutionPlan>> { | |
| let schema = Arc::new(Schema::new(vec![Field::new(name, DataType::Int32, false)])); | |
| let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(Int32Array::from(values))])?; | |
| Ok(Arc::new(TestMemoryExec::try_new( | |
| &[vec![batch]], | |
| schema, | |
| None, | |
| )?)) | |
| } | |
| // Verify that `common_prefix_len` correctly computes prefixes of small fixed | |
| // sizes. This ensures compiler optimizations do not incorrectly transform | |
| // the function for small lengths. | |
| #[test] | |
| fn has_common_prefix_len_been_optimized_into_bogusness() { | |
| let cpl = common_prefix_len(&[0; 8], &[0; 8]); | |
| assert_eq!(cpl, 8); | |
| let cpl = common_prefix_len(&[0; 4], &[0; 4]); | |
| assert_eq!(cpl, 4); | |
| let cpl = common_prefix_len(&[0; 2], &[0; 2]); | |
| assert_eq!(cpl, 2); | |
| let cpl = common_prefix_len(&[0; 1], &[0; 1]); | |
| assert_eq!(cpl, 1); | |
| } | |
| #[tokio::test] | |
| async fn test_sort_i32() -> Result<()> { | |
| MemManager::init(100); | |
| let session_ctx = SessionContext::new(); | |
| let task_ctx = session_ctx.task_ctx(); | |
| let input = build_table( | |
| ("a", &vec![9, 8, 7, 6, 5, 4, 3, 2, 1, 0]), | |
| ("b", &vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), | |
| ("c", &vec![5, 6, 7, 8, 9, 0, 1, 2, 3, 4]), | |
| )?; | |
| let sort_exprs = vec![PhysicalSortExpr { | |
| expr: Arc::new(Column::new("a", 0)), | |
| options: SortOptions::default(), | |
| }]; | |
| let sort = SortExec::new(input, sort_exprs, Some(6), 0); | |
| let output = sort.execute(0, task_ctx)?; | |
| let batches = common::collect(output).await?; | |
| let expected = vec![ | |
| "+---+---+---+", | |
| "| a | b | c |", | |
| "+---+---+---+", | |
| "| 0 | 9 | 4 |", | |
| "| 1 | 8 | 3 |", | |
| "| 2 | 7 | 2 |", | |
| "| 3 | 6 | 1 |", | |
| "| 4 | 5 | 0 |", | |
| "| 5 | 4 | 9 |", | |
| "+---+---+---+", | |
| ]; | |
| assert_batches_eq!(expected, &batches); | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn test_sort_projected_with_key_rows_excludes_sort_key() -> Result<()> { | |
| MemManager::init(100); | |
| let task_ctx = SessionContext::new().task_ctx(); | |
| let input = build_table( | |
| ("key", &vec![3, 1, 2]), | |
| ("value", &vec![30, 10, 20]), | |
| ("unused", &vec![300, 100, 200]), | |
| )?; | |
| let sort = SortExec::new( | |
| input, | |
| vec![PhysicalSortExpr { | |
| expr: Arc::new(Column::new("key", 0)), | |
| options: SortOptions::default(), | |
| }], | |
| None, | |
| 0, | |
| ); | |
| let output = sort.execute_projected_with_key_rows(0, task_ctx, &[1])?; | |
| assert_eq!(output.schema().fields().len(), 1); | |
| assert_eq!(output.schema().field(0).name(), "value"); | |
| let batches = output | |
| .map_ok(|batch| batch.batch) | |
| .try_collect::<Vec<_>>() | |
| .await?; | |
| let expected = vec![ | |
| "+-------+", | |
| "| value |", | |
| "+-------+", | |
| "| 10 |", | |
| "| 20 |", | |
| "| 30 |", | |
| "+-------+", | |
| ]; | |
| assert_batches_eq!(expected, &batches); | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn test_top_k_with_only_sort_column() -> Result<()> { | |
| MemManager::init(100); | |
| let session_ctx = SessionContext::new(); | |
| let task_ctx = session_ctx.task_ctx(); | |
| let input = build_single_column_table("id", (0..10).rev().collect())?; | |
| let sort_exprs = vec![PhysicalSortExpr { | |
| expr: Arc::new(Column::new("id", 0)), | |
| options: SortOptions::default(), | |
| }]; | |
| let sort = SortExec::new(input, sort_exprs, Some(6), 0); | |
| let output = sort.execute(0, task_ctx)?; | |
| let batches = common::collect(output).await?; | |
| let expected = r#"+----+ | |
| | id | | |
| +----+ | |
| | 0 | | |
| | 1 | | |
| | 2 | | |
| | 3 | | |
| | 4 | | |
| | 5 | | |
| +----+"#; | |
| assert_batches_eq!(expected.lines().collect::<Vec<_>>(), &batches); | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn test_sort_i32_with_skip() -> Result<()> { | |
| MemManager::init(100); | |
| let session_ctx = SessionContext::new(); | |
| let task_ctx = session_ctx.task_ctx(); | |
| let input = build_table( | |
| ("a", &vec![9, 8, 7, 6, 5, 4, 3, 2, 1, 0]), | |
| ("b", &vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), | |
| ("c", &vec![5, 6, 7, 8, 9, 0, 1, 2, 3, 4]), | |
| )?; | |
| let sort_exprs = vec![PhysicalSortExpr { | |
| expr: Arc::new(Column::new("a", 0)), | |
| options: SortOptions::default(), | |
| }]; | |
| let sort = SortExec::new(input, sort_exprs, Some(8), 3); | |
| let output = sort.execute(0, task_ctx)?; | |
| let batches = common::collect(output).await?; | |
| let expected = vec![ | |
| "+---+---+---+", | |
| "| a | b | c |", | |
| "+---+---+---+", | |
| "| 3 | 6 | 1 |", | |
| "| 4 | 5 | 0 |", | |
| "| 5 | 4 | 9 |", | |
| "| 6 | 3 | 8 |", | |
| "| 7 | 2 | 7 |", | |
| "+---+---+---+", | |
| ]; | |
| assert_batches_eq!(expected, &batches); | |
| Ok(()) | |
| } | |
| mod default_sort_tests { | |
| use arrow::{compute::concat_batches, record_batch::RecordBatchOptions}; | |
| use datafusion::{physical_expr::PhysicalExpr, physical_plan::ExecutionPlanProperties}; | |
| use super::*; | |
| use crate::sort_exec::create_default_ascending_sort_exec; | |
| #[tokio::test] | |
| async fn test_default_sort_zero_column_batches() -> Result<()> { | |
| MemManager::init(100); | |
| let schema = Arc::new(Schema::empty()); | |
| let row_counts: &[&[usize]] = &[&[1], &[0, 4, 0, 100001], &[0], &[]]; | |
| let mut partitions = vec![]; | |
| for rows in row_counts { | |
| let mut batches = vec![]; | |
| for &num_rows in *rows { | |
| batches.push(RecordBatch::try_new_with_options( | |
| schema.clone(), | |
| vec![], | |
| &RecordBatchOptions::new().with_row_count(Some(num_rows)), | |
| )?); | |
| } | |
| partitions.push(batches); | |
| } | |
| let input = Arc::new(TestMemoryExec::try_new(&partitions, schema.clone(), None)?); | |
| let sort = create_default_ascending_sort_exec(input, &[], None, true); | |
| assert_eq!(sort.schema(), schema); | |
| assert_eq!( | |
| sort.output_partitioning().partition_count(), | |
| partitions.len() | |
| ); | |
| let task_ctx = SessionContext::new().task_ctx(); | |
| for (partition, rows) in row_counts.iter().enumerate() { | |
| let batches = common::collect(sort.execute(partition, task_ctx.clone())?).await?; | |
| assert!(batches.iter().all(|batch| batch.num_columns() == 0)); | |
| assert_eq!( | |
| batches.iter().map(RecordBatch::num_rows).sum::<usize>(), | |
| rows.iter().sum::<usize>(), | |
| ); | |
| if rows.is_empty() { | |
| assert!(batches.is_empty()); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn test_default_sort_values() -> Result<()> { | |
| MemManager::init(100); | |
| for (with_key, expected) in [(false, [3, 1, 2]), (true, [1, 2, 3])] { | |
| let input = build_single_column_table("id", vec![3, 1, 2])?; | |
| let keys: Vec<Arc<dyn PhysicalExpr>> = if with_key { | |
| vec![Arc::new(Column::new("id", 0))] | |
| } else { | |
| vec![] | |
| }; | |
| let sort = create_default_ascending_sort_exec(input, &keys, None, true); | |
| let batches = | |
| common::collect(sort.execute(0, SessionContext::new().task_ctx())?).await?; | |
| let batch = concat_batches(&sort.schema(), &batches)?; | |
| assert_eq!( | |
| batch.column(0).as_any().downcast_ref::<Int32Array>(), | |
| Some(&Int32Array::from(expected.to_vec())), | |
| ); | |
| } | |
| Ok(()) | |
| } | |
| } | |
| } | |
| #[cfg(test)] | |
| mod fuzztest { | |
| #![allow(deprecated)] | |
| use std::{sync::Arc, time::Instant}; | |
| use arrow::{ | |
| array::{Array, ArrayRef, Int64Array, PrimitiveArray, StringArray, UInt32Array}, | |
| compute::{SortOptions, concat_batches}, | |
| datatypes::{ | |
| ArrowPrimitiveType, Date32Type, Date64Type, TimestampMicrosecondType, UInt64Type, | |
| }, | |
| record_batch::RecordBatch, | |
| }; | |
| use auron_memmgr::MemManager; | |
| use datafusion::{ | |
| common::{Result, stats::Precision}, | |
| physical_expr::{LexOrdering, PhysicalSortExpr, expressions::Column}, | |
| physical_plan::{ExecutionPlan, test::TestMemoryExec}, | |
| prelude::{SessionConfig, SessionContext}, | |
| }; | |
| use crate::sort_exec::SortExec; | |
| /// Benchmark helper: build a single Int64 column where each value is | |
| /// repeated `repeat` times, shuffled, to simulate TPC-H | |
| /// lineitem.l_orderkey distribution. | |
| fn build_repeated_i64_batch(num_rows: usize, repeat: usize, seed: u64) -> RecordBatch { | |
| use rand::{Rng, SeedableRng}; | |
| let unique_keys = num_rows.div_ceil(repeat); | |
| let mut values: Vec<i64> = (0..unique_keys) | |
| .flat_map(|v| std::iter::repeat_n(v as i64, repeat)) | |
| .take(num_rows) | |
| .collect(); | |
| let mut rng = rand::rngs::StdRng::seed_from_u64(seed); | |
| rand::seq::SliceRandom::shuffle(values.as_mut_slice(), &mut rng); | |
| let schema = Arc::new(arrow::datatypes::Schema::new(vec![ | |
| arrow::datatypes::Field::new("l_orderkey", arrow::datatypes::DataType::Int64, false), | |
| ])); | |
| RecordBatch::try_new(schema, vec![Arc::new(Int64Array::from(values)) as ArrayRef]) | |
| .expect("failed to create benchmark batch") | |
| } | |
| async fn bench_sort_repeat(repeat: usize, mem: usize, use_auron: bool) -> Result<(usize, f64)> { | |
| bench_sort_repeat_with_limit(repeat, mem, use_auron, None).await | |
| } | |
| /// Variant of `bench_sort_repeat` that can pass a Top-N `limit` to the | |
| /// Auron `SortExec`, so the `sort_to_indices(limit)` partial-sort path | |
| /// can be exercised (vs. a full sort when `limit` is `None`). | |
| async fn bench_sort_repeat_with_limit( | |
| repeat: usize, | |
| mem: usize, | |
| use_auron: bool, | |
| limit: Option<usize>, | |
| ) -> Result<(usize, f64)> { | |
| MemManager::init(mem); | |
| let session_ctx = | |
| SessionContext::new_with_config(SessionConfig::new().with_batch_size(10000)); | |
| let task_ctx = session_ctx.task_ctx(); | |
| let num_rows = 1_000_000; | |
| let batch = build_repeated_i64_batch(num_rows, repeat, 42); | |
| let schema = batch.schema(); | |
| let sort_exprs = vec![PhysicalSortExpr { | |
| expr: Arc::new(Column::new("l_orderkey", 0)), | |
| options: SortOptions::default(), | |
| }]; | |
| let input = Arc::new(TestMemoryExec::try_new( | |
| &[vec![batch]], | |
| schema.clone(), | |
| None, | |
| )?); | |
| let sort: Arc<dyn ExecutionPlan> = if use_auron { | |
| Arc::new(SortExec::new(input, sort_exprs.clone(), limit, 0)) | |
| } else { | |
| Arc::new( | |
| datafusion::physical_plan::sorts::sort::SortExec::new( | |
| LexOrdering::new(sort_exprs.iter().cloned()).expect("invalid sort exprs"), | |
| input, | |
| ) | |
| .with_fetch(limit), | |
| ) | |
| }; | |
| let start = Instant::now(); | |
| let output = datafusion::physical_plan::collect(sort.clone(), task_ctx.clone()).await?; | |
| let elapsed = start.elapsed().as_secs_f64(); | |
| let total_rows: usize = output.iter().map(|b| b.num_rows()).sum(); | |
| Ok((total_rows, elapsed)) | |
| } | |
| #[tokio::test] | |
| #[ignore = "manual benchmark"] | |
| async fn bench_native_sort_varying_repeat_in_mem() -> Result<()> { | |
| for repeat in [1usize, 4, 20, 100] { | |
| let (_, elapsed_auron) = bench_sort_repeat(repeat, 1_000_000_000, true).await?; | |
| let (_, elapsed_df) = bench_sort_repeat(repeat, 1_000_000_000, false).await?; | |
| eprintln!( | |
| "[sort in-mem] repeat={:>3}, auron={:.3}s, datafusion={:.3}s, speedup(df/auron)={:.2}x", | |
| repeat, | |
| elapsed_auron, | |
| elapsed_df, | |
| elapsed_auron / elapsed_df | |
| ); | |
| } | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| #[ignore = "manual benchmark"] | |
| async fn bench_native_sort_topn_limit_in_mem() -> Result<()> { | |
| // Top-N scenario: limit=10000 << num_rows=1M, so the primitive fast | |
| // path's `sort_to_indices(Some(limit))` partial sort should be cheaper | |
| // than a full sort. Compares Auron vs DataFusion, both with limit. | |
| let limit = 10_000usize; | |
| for repeat in [1usize, 100] { | |
| let (_, elapsed_auron) = | |
| bench_sort_repeat_with_limit(repeat, 1_000_000_000, true, Some(limit)).await?; | |
| let (_, elapsed_df) = | |
| bench_sort_repeat_with_limit(repeat, 1_000_000_000, false, Some(limit)).await?; | |
| eprintln!( | |
| "[sort top-N limit={limit}] repeat={repeat:>3}, auron={elapsed_auron:.3}s, datafusion={elapsed_df:.3}s, speedup(df/auron)={:.2}x", | |
| elapsed_auron / elapsed_df | |
| ); | |
| } | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| #[ignore = "manual benchmark"] | |
| async fn bench_native_sort_varying_repeat_external() -> Result<()> { | |
| // NOTE: this measures Auron's external (spilling) sort in isolation. | |
| // DataFusion's native SortExec has no spilling path, so under the same | |
| // 2 MB `MemManager` limit Auron spills while DataFusion keeps sorting | |
| // fully in memory — the two are not comparable. We therefore report | |
| // Auron's spill cost on its own; use the in-mem group for a fair | |
| // Auron-vs-DataFusion comparison (both stay in memory at 1 GB). | |
| for repeat in [1usize, 4, 20, 100] { | |
| let (_, elapsed_auron) = bench_sort_repeat(repeat, 2_000_000, true).await?; | |
| eprintln!( | |
| "[sort external/spill, auron-only] repeat={repeat:>3}, auron={elapsed_auron:.3}s" | |
| ); | |
| } | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn fuzztest_in_mem_sorting() -> Result<()> { | |
| let time_start = Instant::now(); | |
| fuzztest_with_mem_conf(1000000000).await?; | |
| eprintln!("fuzztest_in_mem_sorting_time: {:?}", time_start.elapsed()); | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn fuzztest_external_sorting() -> Result<()> { | |
| let time_start = Instant::now(); | |
| fuzztest_with_mem_conf(10000).await?; | |
| eprintln!("fuzztest_external_sorting_time: {:?}", time_start.elapsed()); | |
| Ok(()) | |
| } | |
| /// Builds `num_batches` Int64-keyed batches for exercising the primitive | |
| /// fast path (single direct `Column` key, so `PrimitiveSortKey::try_new` | |
| /// applies). | |
| /// | |
| /// Without a payload column, keys are drawn from a small domain with ~5% | |
| /// nulls, so duplicates and nulls are heavily exercised while tied rows | |
| /// stay indistinguishable (full-output comparison is meaningful). With a | |
| /// payload column, keys are unique global ids (no nulls) so the total | |
| /// order is well-defined even though rows carry distinct payload values. | |
| fn build_i64_fast_path_batches( | |
| num_batches: usize, | |
| rows_per_batch: usize, | |
| with_payload: bool, | |
| seed: u64, | |
| ) -> Vec<RecordBatch> { | |
| use rand::{Rng, SeedableRng, seq::SliceRandom}; | |
| let total = num_batches * rows_per_batch; | |
| let mut rng = rand::rngs::StdRng::seed_from_u64(seed); | |
| let keys: Vec<Option<i64>> = if with_payload { | |
| let mut ids: Vec<Option<i64>> = (0..total as i64).map(Some).collect(); | |
| ids.shuffle(&mut rng); | |
| ids | |
| } else { | |
| std::iter::repeat_with(|| { | |
| if rng.random_ratio(1, 20) { | |
| None | |
| } else { | |
| Some(rng.random_range(0..1000)) | |
| } | |
| }) | |
| .take(total) | |
| .collect() | |
| }; | |
| let schema = Arc::new(arrow::datatypes::Schema::new( | |
| std::iter::once(arrow::datatypes::Field::new( | |
| "c0", | |
| arrow::datatypes::DataType::Int64, | |
| true, | |
| )) | |
| .chain(with_payload.then(|| { | |
| arrow::datatypes::Field::new("v", arrow::datatypes::DataType::UInt32, true) | |
| })) | |
| .collect::<Vec<_>>(), | |
| )); | |
| keys.chunks(rows_per_batch) | |
| .map(|chunk| { | |
| let mut columns: Vec<ArrayRef> = vec![Arc::new(Int64Array::from(chunk.to_vec()))]; | |
| if with_payload { | |
| let payload: UInt32Array = std::iter::repeat_with(|| rng.random::<u32>()) | |
| .take(chunk.len()) | |
| .collect(); | |
| columns.push(Arc::new(payload)); | |
| } | |
| RecordBatch::try_new(schema.clone(), columns).expect("failed to create test batch") | |
| }) | |
| .collect() | |
| } | |
| /// Runs a single-column primitive sort with the given options/limit | |
| /// through Auron's `SortExec` (primitive fast path) or DataFusion's | |
| /// `SortExec`, and returns the concatenated output batch. | |
| async fn collect_primitive_sort_output( | |
| batches: &[RecordBatch], | |
| options: SortOptions, | |
| limit: Option<usize>, | |
| use_auron: bool, | |
| ) -> Result<RecordBatch> { | |
| let session_ctx = | |
| SessionContext::new_with_config(SessionConfig::new().with_batch_size(10000)); | |
| let task_ctx = session_ctx.task_ctx(); | |
| let schema = batches[0].schema(); | |
| let sort_exprs = vec![PhysicalSortExpr { | |
| expr: Arc::new(Column::new("c0", 0)), | |
| options, | |
| }]; | |
| let input = Arc::new(TestMemoryExec::try_new( | |
| &[batches.to_vec()], | |
| schema.clone(), | |
| None, | |
| )?); | |
| let sort: Arc<dyn ExecutionPlan> = if use_auron { | |
| Arc::new(SortExec::new(input, sort_exprs.clone(), limit, 0)) | |
| } else { | |
| Arc::new( | |
| datafusion::physical_plan::sorts::sort::SortExec::new( | |
| LexOrdering::new(sort_exprs.iter().cloned()).expect("invalid sort exprs"), | |
| input, | |
| ) | |
| .with_fetch(limit), | |
| ) | |
| }; | |
| let output = datafusion::physical_plan::collect(sort, task_ctx).await?; | |
| Ok(concat_batches(&schema, &output)?) | |
| } | |
| /// Correctness coverage for the primitive fast path (single-column | |
| /// primitive key sorted via Arrow `sort_to_indices`). The fuzz tests | |
| /// above use string keys and therefore only exercise the RowConverter | |
| /// path, while the benchmarks above are `#[ignore]`d and only measure | |
| /// elapsed time. These tests feed multiple batches (so the K-way merge | |
| /// is exercised), cover ascending/descending and nulls-first/nulls-last | |
| /// orderings as well as a Top-N limit, and compare the complete output | |
| /// with DataFusion's `SortExec`. | |
| async fn fuzztest_primitive_fast_path_with_conf( | |
| mem: usize, | |
| limit: Option<usize>, | |
| with_payload: bool, | |
| ) -> Result<()> { | |
| MemManager::init(mem); | |
| let batches = build_i64_fast_path_batches(51, 10000, with_payload, 42); | |
| let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum(); | |
| for descending in [false, true] { | |
| for nulls_first in [false, true] { | |
| let options = SortOptions { | |
| descending, | |
| nulls_first, | |
| }; | |
| let a = collect_primitive_sort_output(&batches, options, limit, true).await?; | |
| let b = collect_primitive_sort_output(&batches, options, limit, false).await?; | |
| assert!( | |
| a == b, | |
| "primitive fast path output mismatch: descending={descending}, \ | |
| nulls_first={nulls_first}, limit={limit:?}, with_payload={with_payload}" | |
| ); | |
| assert_eq!(a.num_rows(), limit.unwrap_or(usize::MAX).min(total_rows)); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn fuzztest_primitive_fast_path_in_mem_sorting() -> Result<()> { | |
| fuzztest_primitive_fast_path_with_conf(1000000000, None, false).await | |
| } | |
| #[tokio::test] | |
| async fn fuzztest_primitive_fast_path_in_mem_sorting_with_payload() -> Result<()> { | |
| fuzztest_primitive_fast_path_with_conf(1000000000, None, true).await | |
| } | |
| #[tokio::test] | |
| async fn fuzztest_primitive_fast_path_external_sorting() -> Result<()> { | |
| fuzztest_primitive_fast_path_with_conf(10000, None, false).await | |
| } | |
| #[tokio::test] | |
| async fn fuzztest_primitive_fast_path_topn() -> Result<()> { | |
| // Top-N: limit is much smaller than the total row count, so the fast | |
| // path takes the `sort_to_indices(Some(limit))` partial-sort branch. | |
| fuzztest_primitive_fast_path_with_conf(1000000000, Some(9999), false).await | |
| } | |
| /// Builds single-column primitive batches from `keys`, chunked into | |
| /// `rows_per_batch`-sized batches. Used to exercise the primitive fast | |
| /// path across the advertised key types (not just Int64). | |
| fn build_primitive_fast_path_batches<T>( | |
| keys: Vec<Option<T::Native>>, | |
| rows_per_batch: usize, | |
| ) -> Vec<RecordBatch> | |
| where | |
| T: ArrowPrimitiveType, | |
| PrimitiveArray<T>: From<Vec<Option<T::Native>>>, | |
| { | |
| let schema = Arc::new(arrow::datatypes::Schema::new(vec![ | |
| arrow::datatypes::Field::new("c0", T::DATA_TYPE, true), | |
| ])); | |
| keys.chunks(rows_per_batch) | |
| .map(|chunk| { | |
| RecordBatch::try_new( | |
| schema.clone(), | |
| vec![Arc::new(PrimitiveArray::<T>::from(chunk.to_vec())) as ArrayRef], | |
| ) | |
| .expect("failed to create test batch") | |
| }) | |
| .collect() | |
| } | |
| /// Parameterized correctness check for one primitive key type: sorts | |
| /// `extremes` plus randomly generated keys from `domain` (small domain | |
| /// with ~5% nulls, so duplicates and nulls are heavily exercised) with | |
| /// both Auron and DataFusion under all asc/desc x nulls-first/nulls-last | |
| /// combinations and requires identical output. `extremes` should include | |
| /// the type's min/max values so ordering-semantics bugs (e.g. | |
| /// signed/unsigned mixups) cannot hide inside the random domain. | |
| async fn check_primitive_type_fast_path<T>( | |
| extremes: &[T::Native], | |
| domain: std::ops::Range<T::Native>, | |
| ) -> Result<()> | |
| where | |
| T: ArrowPrimitiveType, | |
| T::Native: rand::distr::uniform::SampleUniform, | |
| PrimitiveArray<T>: From<Vec<Option<T::Native>>>, | |
| { | |
| use rand::{Rng, SeedableRng, seq::SliceRandom}; | |
| MemManager::init(1000000000); | |
| let total = 51000; | |
| let mut rng = rand::rngs::StdRng::seed_from_u64(42); | |
| let mut keys: Vec<Option<T::Native>> = extremes.iter().map(|v| Some(*v)).collect(); | |
| keys.extend( | |
| std::iter::repeat_with(|| { | |
| if rng.random_ratio(1, 20) { | |
| None | |
| } else { | |
| Some(rng.random_range(domain.clone())) | |
| } | |
| }) | |
| .take(total - extremes.len()), | |
| ); | |
| keys.shuffle(&mut rng); | |
| let batches = build_primitive_fast_path_batches::<T>(keys, 1000); | |
| for descending in [false, true] { | |
| for nulls_first in [false, true] { | |
| let options = SortOptions { | |
| descending, | |
| nulls_first, | |
| }; | |
| let a = collect_primitive_sort_output(&batches, options, None, true).await?; | |
| let b = collect_primitive_sort_output(&batches, options, None, false).await?; | |
| assert!( | |
| a == b, | |
| "primitive fast path output mismatch: descending={descending}, \ | |
| nulls_first={nulls_first}" | |
| ); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn fuzztest_primitive_fast_path_uint64() -> Result<()> { | |
| // extremes above i64::MAX catch signed/unsigned ordering mixups | |
| check_primitive_type_fast_path::<UInt64Type>( | |
| &[0, 1, i64::MAX as u64, i64::MAX as u64 + 1, u64::MAX], | |
| 0..1000, | |
| ) | |
| .await | |
| } | |
| #[tokio::test] | |
| async fn fuzztest_primitive_fast_path_date32() -> Result<()> { | |
| check_primitive_type_fast_path::<Date32Type>(&[i32::MIN, -1, 0, 1, i32::MAX], -1000..1000) | |
| .await | |
| } | |
| #[tokio::test] | |
| async fn fuzztest_primitive_fast_path_date64() -> Result<()> { | |
| check_primitive_type_fast_path::<Date64Type>(&[i64::MIN, -1, 0, 1, i64::MAX], -1000..1000) | |
| .await | |
| } | |
| #[tokio::test] | |
| async fn fuzztest_primitive_fast_path_timestamp_us() -> Result<()> { | |
| check_primitive_type_fast_path::<TimestampMicrosecondType>( | |
| &[i64::MIN, -1, 0, 1, i64::MAX], | |
| -1_000_000..1_000_000, | |
| ) | |
| .await | |
| } | |
| /// Combined coverage: nullable key + payload column + Top-N limit. | |
| /// Duplicate keys make the relative order of tied payload rows | |
| /// implementation-defined, so the payload is a deterministic function of | |
| /// the key: the key column must match DataFusion exactly (null ordering | |
| /// included) and every payload must be aligned with its key. | |
| #[tokio::test] | |
| async fn fuzztest_primitive_fast_path_topn_nullable_key_with_payload() -> Result<()> { | |
| use rand::{Rng, SeedableRng}; | |
| const NULL_PAYLOAD: u32 = u32::MAX; | |
| let payload_of = |key: i64| (key as u32).wrapping_mul(31).wrapping_add(7); | |
| MemManager::init(1000000000); | |
| let total = 51000; | |
| let limit = 9999; | |
| let mut rng = rand::rngs::StdRng::seed_from_u64(42); | |
| let keys: Vec<Option<i64>> = std::iter::repeat_with(|| { | |
| if rng.random_ratio(1, 20) { | |
| None | |
| } else { | |
| Some(rng.random_range(0..1000)) | |
| } | |
| }) | |
| .take(total) | |
| .collect(); | |
| let schema = Arc::new(arrow::datatypes::Schema::new(vec![ | |
| arrow::datatypes::Field::new("c0", arrow::datatypes::DataType::Int64, true), | |
| arrow::datatypes::Field::new("v", arrow::datatypes::DataType::UInt32, false), | |
| ])); | |
| let batches: Vec<RecordBatch> = keys | |
| .chunks(1000) | |
| .map(|chunk| { | |
| let payload: UInt32Array = chunk | |
| .iter() | |
| .map(|key| key.map_or(NULL_PAYLOAD, payload_of)) | |
| .collect(); | |
| RecordBatch::try_new( | |
| schema.clone(), | |
| vec![ | |
| Arc::new(Int64Array::from(chunk.to_vec())) as ArrayRef, | |
| Arc::new(payload), | |
| ], | |
| ) | |
| .expect("failed to create test batch") | |
| }) | |
| .collect(); | |
| for descending in [false, true] { | |
| for nulls_first in [false, true] { | |
| let options = SortOptions { | |
| descending, | |
| nulls_first, | |
| }; | |
| let a = collect_primitive_sort_output(&batches, options, Some(limit), true).await?; | |
| let b = | |
| collect_primitive_sort_output(&batches, options, Some(limit), false).await?; | |
| assert_eq!(a.num_rows(), limit); | |
| let keys_a = a | |
| .column(0) | |
| .as_any() | |
| .downcast_ref::<Int64Array>() | |
| .expect("key column must be Int64"); | |
| let keys_b = b | |
| .column(0) | |
| .as_any() | |
| .downcast_ref::<Int64Array>() | |
| .expect("key column must be Int64"); | |
| assert_eq!( | |
| keys_a, keys_b, | |
| "key ordering mismatch: descending={descending}, nulls_first={nulls_first}" | |
| ); | |
| let payload_a = a | |
| .column(1) | |
| .as_any() | |
| .downcast_ref::<UInt32Array>() | |
| .expect("payload column must be UInt32"); | |
| for row_idx in 0..a.num_rows() { | |
| let expected = if keys_a.is_null(row_idx) { | |
| NULL_PAYLOAD | |
| } else { | |
| payload_of(keys_a.value(row_idx)) | |
| }; | |
| assert_eq!( | |
| payload_a.value(row_idx), | |
| expected, | |
| "payload misaligned at row {row_idx}: descending={descending}, \ | |
| nulls_first={nulls_first}" | |
| ); | |
| } | |
| } | |
| } | |
| Ok(()) | |
| } | |
| async fn fuzztest_with_mem_conf(mem: usize) -> Result<()> { | |
| MemManager::init(mem); | |
| let session_ctx = | |
| SessionContext::new_with_config(SessionConfig::new().with_batch_size(10000)); | |
| let task_ctx = session_ctx.task_ctx(); | |
| let n = 1234567; | |
| // generate random batch for fuzzying | |
| let mut batches = vec![]; | |
| let mut num_rows = 0; | |
| while num_rows < n { | |
| let rand_key1: ArrayRef = Arc::new( | |
| std::iter::repeat_with(|| Some(format!("{}", rand::random::<u32>()))) | |
| .take((n - num_rows).min(10000)) | |
| .collect::<StringArray>(), | |
| ); | |
| let rand_key2: ArrayRef = Arc::new( | |
| std::iter::repeat_with(|| rand::random::<u32>()) | |
| .take((n - num_rows).min(10000)) | |
| .collect::<UInt32Array>(), | |
| ); | |
| let rand_val1: ArrayRef = Arc::new( | |
| std::iter::repeat_with(|| rand::random::<u32>()) | |
| .take((n - num_rows).min(10000)) | |
| .collect::<UInt32Array>(), | |
| ); | |
| let rand_val2: ArrayRef = Arc::new( | |
| std::iter::repeat_with(|| rand::random::<u32>()) | |
| .take((n - num_rows).min(10000)) | |
| .collect::<UInt32Array>(), | |
| ); | |
| let batch = RecordBatch::try_from_iter_with_nullable(vec![ | |
| ("k1", rand_key1, true), | |
| ("k2", rand_key2, true), | |
| ("v1", rand_val1, true), | |
| ("v2", rand_val2, true), | |
| ])?; | |
| num_rows += batch.num_rows(); | |
| batches.push(batch); | |
| } | |
| let schema = batches[0].schema(); | |
| let sort_exprs = vec![ | |
| PhysicalSortExpr { | |
| expr: Arc::new(Column::new("k1", 0)), | |
| options: SortOptions::default(), | |
| }, | |
| PhysicalSortExpr { | |
| expr: Arc::new(Column::new("k2", 1)), | |
| options: SortOptions::default(), | |
| }, | |
| ]; | |
| let input = Arc::new(TestMemoryExec::try_new( | |
| &[batches.clone()], | |
| schema.clone(), | |
| None, | |
| )?); | |
| let sort = Arc::new(SortExec::new(input, sort_exprs.clone(), None, 0)); | |
| let output = datafusion::physical_plan::collect(sort.clone(), task_ctx.clone()).await?; | |
| let a = concat_batches(&schema, &output)?; | |
| let a_row_count = sort.clone().partition_statistics(None)?.num_rows; | |
| let input = Arc::new(TestMemoryExec::try_new( | |
| &[batches.clone()], | |
| schema.clone(), | |
| None, | |
| )?); | |
| let sort = Arc::new(datafusion::physical_plan::sorts::sort::SortExec::new( | |
| LexOrdering::new(sort_exprs.iter().cloned()).expect("invalid sort exprs"), | |
| input, | |
| )); | |
| let output = datafusion::physical_plan::collect(sort.clone(), task_ctx.clone()).await?; | |
| let b = concat_batches(&schema, &output)?; | |
| let b_row_count = sort.clone().partition_statistics(None)?.num_rows; | |
| assert_eq!(a.num_rows(), b.num_rows()); | |
| assert_eq!(a_row_count, b_row_count); | |
| assert_eq!(a.num_rows(), n); | |
| assert!(a == b); | |
| Ok(()) | |
| } | |
| } |