@@ -585,4 +585,112 @@ mod test {
585585 let _ = fs:: remove_file ( "/tmp/rr_data_1.out" ) ;
586586 let _ = fs:: remove_file ( "/tmp/rr_index_1.out" ) ;
587587 }
588+
589+ /// Test that batch coalescing in BufBatchWriter reduces output size by
590+ /// writing fewer, larger IPC blocks instead of many small ones.
591+ #[ test]
592+ #[ cfg_attr( miri, ignore) ]
593+ fn test_batch_coalescing_reduces_size ( ) {
594+ use crate :: execution:: shuffle:: writers:: BufBatchWriter ;
595+ use arrow:: array:: Int32Array ;
596+
597+ // Create a wide schema to amplify per-block schema overhead
598+ let fields: Vec < Field > = ( 0 ..20 )
599+ . map ( |i| Field :: new ( format ! ( "col_{i}" ) , DataType :: Int32 , false ) )
600+ . collect ( ) ;
601+ let schema = Arc :: new ( Schema :: new ( fields) ) ;
602+
603+ // Create many small batches (50 rows each)
604+ let small_batches: Vec < RecordBatch > = ( 0 ..100 )
605+ . map ( |batch_idx| {
606+ let columns: Vec < Arc < dyn Array > > = ( 0 ..20 )
607+ . map ( |col_idx| {
608+ let values: Vec < i32 > = ( 0 ..50 )
609+ . map ( |row| batch_idx * 50 + row + col_idx * 1000 )
610+ . collect ( ) ;
611+ Arc :: new ( Int32Array :: from ( values) ) as Arc < dyn Array >
612+ } )
613+ . collect ( ) ;
614+ RecordBatch :: try_new ( Arc :: clone ( & schema) , columns) . unwrap ( )
615+ } )
616+ . collect ( ) ;
617+
618+ let codec = CompressionCodec :: Lz4Frame ;
619+ let encode_time = Time :: default ( ) ;
620+ let write_time = Time :: default ( ) ;
621+
622+ // Write with coalescing (batch_size=8192)
623+ let mut coalesced_output = Vec :: new ( ) ;
624+ {
625+ let mut writer = ShuffleBlockWriter :: try_new ( schema. as_ref ( ) , codec. clone ( ) ) . unwrap ( ) ;
626+ let mut buf_writer = BufBatchWriter :: new (
627+ & mut writer,
628+ Cursor :: new ( & mut coalesced_output) ,
629+ 1024 * 1024 ,
630+ 8192 ,
631+ ) ;
632+ for batch in & small_batches {
633+ buf_writer. write ( batch, & encode_time, & write_time) . unwrap ( ) ;
634+ }
635+ buf_writer. flush ( & encode_time, & write_time) . unwrap ( ) ;
636+ }
637+
638+ // Write without coalescing (batch_size=1)
639+ let mut uncoalesced_output = Vec :: new ( ) ;
640+ {
641+ let mut writer = ShuffleBlockWriter :: try_new ( schema. as_ref ( ) , codec. clone ( ) ) . unwrap ( ) ;
642+ let mut buf_writer = BufBatchWriter :: new (
643+ & mut writer,
644+ Cursor :: new ( & mut uncoalesced_output) ,
645+ 1024 * 1024 ,
646+ 1 ,
647+ ) ;
648+ for batch in & small_batches {
649+ buf_writer. write ( batch, & encode_time, & write_time) . unwrap ( ) ;
650+ }
651+ buf_writer. flush ( & encode_time, & write_time) . unwrap ( ) ;
652+ }
653+
654+ // Coalesced output should be smaller due to fewer IPC schema blocks
655+ assert ! (
656+ coalesced_output. len( ) < uncoalesced_output. len( ) ,
657+ "Coalesced output ({} bytes) should be smaller than uncoalesced ({} bytes)" ,
658+ coalesced_output. len( ) ,
659+ uncoalesced_output. len( )
660+ ) ;
661+
662+ // Verify both roundtrip correctly by reading all IPC blocks
663+ let coalesced_rows = read_all_ipc_blocks ( & coalesced_output) ;
664+ let uncoalesced_rows = read_all_ipc_blocks ( & uncoalesced_output) ;
665+ assert_eq ! (
666+ coalesced_rows, 5000 ,
667+ "Coalesced should contain all 5000 rows"
668+ ) ;
669+ assert_eq ! (
670+ uncoalesced_rows, 5000 ,
671+ "Uncoalesced should contain all 5000 rows"
672+ ) ;
673+ }
674+
675+ /// Read all IPC blocks from a byte buffer written by BufBatchWriter/ShuffleBlockWriter,
676+ /// returning the total number of rows.
677+ fn read_all_ipc_blocks ( data : & [ u8 ] ) -> usize {
678+ let mut offset = 0 ;
679+ let mut total_rows = 0 ;
680+ while offset < data. len ( ) {
681+ // First 8 bytes are the IPC length (little-endian u64)
682+ let ipc_length =
683+ u64:: from_le_bytes ( data[ offset..offset + 8 ] . try_into ( ) . unwrap ( ) ) as usize ;
684+ // Skip the 8-byte length prefix; the next 8 bytes are field_count + codec header
685+ let block_start = offset + 8 ;
686+ let block_end = block_start + ipc_length;
687+ // read_ipc_compressed expects data starting after the 16-byte header
688+ // (i.e., after length + field_count), at the codec tag
689+ let ipc_data = & data[ block_start + 8 ..block_end] ;
690+ let batch = read_ipc_compressed ( ipc_data) . unwrap ( ) ;
691+ total_rows += batch. num_rows ( ) ;
692+ offset = block_end;
693+ }
694+ total_rows
695+ }
588696}
0 commit comments