@@ -46,6 +46,7 @@ use datafusion::{
4646} ;
4747use futures_util:: { StreamExt , TryFutureExt , TryStreamExt } ;
4848use itertools:: Itertools ;
49+ use rayon:: prelude:: * ;
4950
5051/// Number of manifest files fetched from the metastore in parallel while
5152/// planning a query.
@@ -572,19 +573,46 @@ async fn collect_from_snapshot(
572573 . try_collect :: < Vec < _ > > ( )
573574 . await ?;
574575
575- let mut manifest_files: Vec < _ > = manifest_files
576+ let manifest_files: Vec < _ > = manifest_files
576577 . into_iter ( )
577578 . flatten ( )
578579 . flat_map ( |file| file. files )
579580 . rev ( )
580581 . collect ( ) ;
581582 let files_before_prune = manifest_files. len ( ) ;
582- let mut pruned_by_filter: Vec < ( String , usize ) > = Vec :: new ( ) ;
583- for filter in filters {
584- let before = manifest_files. len ( ) ;
585- manifest_files. retain ( |file| !file. can_be_pruned ( filter) ) ;
586- pruned_by_filter. push ( ( filter. to_string ( ) , before - manifest_files. len ( ) ) ) ;
583+
584+ // One parallel pass rather than a `retain` per filter. Every
585+ // `can_be_pruned` call linearly scans the file's column list (hundreds of
586+ // entries on wide streams), so a sequential pass per filter over tens of
587+ // thousands of files is hundreds of milliseconds of single threaded work.
588+ // `position` keeps attribution identical to the sequential chain: a file is
589+ // credited to the first filter that would have dropped it.
590+ let prune_reason: Vec < Option < usize > > = manifest_files
591+ . par_iter ( )
592+ . map ( |file| filters. iter ( ) . position ( |filter| file. can_be_pruned ( filter) ) )
593+ . collect ( ) ;
594+
595+ let mut pruned_counts = vec ! [ 0usize ; filters. len( ) ] ;
596+ for reason in prune_reason. iter ( ) . flatten ( ) {
597+ pruned_counts[ * reason] += 1 ;
587598 }
599+ let pruned_by_filter: Vec < ( String , usize ) > = filters
600+ . iter ( )
601+ . zip ( pruned_counts)
602+ . map ( |( filter, count) | ( filter. to_string ( ) , count) )
603+ . collect ( ) ;
604+
605+ // Parallel because of the drops, not the filtering. Pruning discards the
606+ // overwhelming majority of files, and every discarded `File` owns a `Vec`
607+ // of hundreds of `Column`s, each with its own heap allocated name and
608+ // stats. Tens of millions of frees on one thread is hundreds of
609+ // milliseconds; `into_par_iter` spreads them. Order is preserved — the
610+ // `rev()` above establishes the ordering the limit below depends on.
611+ let mut manifest_files: Vec < _ > = manifest_files
612+ . into_par_iter ( )
613+ . zip ( prune_reason)
614+ . filter_map ( |( file, reason) | reason. is_none ( ) . then_some ( file) )
615+ . collect ( ) ;
588616 tracing:: warn!(
589617 stream = %stream_name,
590618 files_before_prune,
0 commit comments