Skip to content

Commit e35b176

Browse files
committed
perf(layout): zone lists before decomposition
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 020a962 commit e35b176

2 files changed

Lines changed: 112 additions & 16 deletions

File tree

vortex-file/src/strategy.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ use vortex_layout::layouts::compressed::CompressingStrategy;
5050
use vortex_layout::layouts::compressed::CompressorPlugin;
5151
use vortex_layout::layouts::dict::writer::DictStrategy;
5252
use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
53+
use vortex_layout::layouts::list::writer::ListLayoutStrategy;
5354
use vortex_layout::layouts::repartition::RepartitionStrategy;
5455
use vortex_layout::layouts::repartition::RepartitionWriterOptions;
5556
use vortex_layout::layouts::table::TableStrategy;
@@ -322,12 +323,14 @@ impl WriteStrategyBuilder {
322323
probe_compressor,
323324
);
324325

326+
let row_block_size = NonZeroUsize::new(self.row_block_size).vortex_expect("must be non 0");
327+
325328
// 2. calculate stats for each row group
326329
let stats = ZonedStrategy::new(
327330
dict,
328331
compress_then_flat.clone(),
329332
ZonedLayoutOptions {
330-
block_size: NonZeroUsize::new(self.row_block_size).vortex_expect("must be non 0"),
333+
block_size: row_block_size,
331334
..Default::default()
332335
},
333336
);
@@ -346,15 +349,36 @@ impl WriteStrategyBuilder {
346349
);
347350

348351
// 0. start with splitting columns
349-
let validity_strategy = CollectStrategy::new(compress_then_flat);
352+
let validity_strategy = CollectStrategy::new(compress_then_flat.clone());
350353

351354
// Take any field overrides from the builder and apply them to the final strategy.
352355
let mut table_strategy =
353356
TableStrategy::new(Arc::new(validity_strategy), Arc::new(repartition))
354357
.with_field_writers(self.field_writers);
355358

356359
if self.use_list_layout {
357-
table_strategy = table_strategy.with_list_layout();
360+
// We need a closure here to enable recursive application of list layout.
361+
table_strategy = table_strategy.with_list_layout_factory(
362+
move |list_layout: ListLayoutStrategy| -> Arc<dyn LayoutStrategy> {
363+
let zoned = ZonedStrategy::new(
364+
list_layout,
365+
compress_then_flat.clone(),
366+
ZonedLayoutOptions {
367+
block_size: row_block_size,
368+
..Default::default()
369+
},
370+
);
371+
Arc::new(RepartitionStrategy::new(
372+
zoned,
373+
RepartitionWriterOptions {
374+
block_size_minimum: 0,
375+
block_len_multiple: row_block_size.get(),
376+
block_size_target: None,
377+
canonicalize: false,
378+
},
379+
))
380+
},
381+
);
358382
}
359383

360384
Arc::new(table_strategy)

vortex-layout/src/layouts/table.rs

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ pub fn use_experimental_list_layout() -> bool {
4545
*USE_EXPERIMENTAL_LIST_LAYOUT
4646
}
4747

48+
type ListLayoutFactory = Arc<dyn Fn(ListLayoutStrategy) -> Arc<dyn LayoutStrategy> + Send + Sync>;
49+
4850
/// A configurable strategy for writing nested tabular data, dispatching each (sub)stream to the
4951
/// structural writer for its dtype.
5052
///
@@ -67,10 +69,11 @@ pub struct TableStrategy {
6769
validity: Arc<dyn LayoutStrategy>,
6870
/// The writer for leaf fields, i.e. anything that is not a struct.
6971
leaf: Arc<dyn LayoutStrategy>,
70-
/// Whether to write list fields using [`ListLayoutStrategy`].
72+
/// Optional factory applied to each dynamically constructed [`ListLayoutStrategy`].
73+
/// Its presence also enables list decomposition.
7174
///
7275
/// [`ListLayoutStrategy`]: crate::layouts::list::writer::ListLayoutStrategy
73-
use_list_layout: bool,
76+
list_layout_factory: Option<ListLayoutFactory>,
7477
}
7578

7679
impl TableStrategy {
@@ -97,7 +100,7 @@ impl TableStrategy {
97100
leaf_writers: Default::default(),
98101
validity,
99102
leaf: fallback,
100-
use_list_layout: false,
103+
list_layout_factory: None,
101104
}
102105
}
103106

@@ -166,8 +169,18 @@ impl TableStrategy {
166169
}
167170

168171
/// Enable writing list fields with [`ListLayoutStrategy`].
169-
pub fn with_list_layout(mut self) -> Self {
170-
self.use_list_layout = true;
172+
pub fn with_list_layout(self) -> Self {
173+
self.with_list_layout_factory(|strategy| Arc::new(strategy))
174+
}
175+
176+
/// Enable list layouts and transform each structural list writer into a physical strategy.
177+
/// The factory receives a fully configured writer and is invoked independently for every list,
178+
/// including nested lists.
179+
pub fn with_list_layout_factory(
180+
mut self,
181+
factory: impl Fn(ListLayoutStrategy) -> Arc<dyn LayoutStrategy> + Send + Sync + 'static,
182+
) -> Self {
183+
self.list_layout_factory = Some(Arc::new(factory));
171184
self
172185
}
173186
}
@@ -210,12 +223,14 @@ impl TableStrategy {
210223
/// The `elements` sub-column is routed back through a clean descended dispatcher so nested
211224
/// structs/lists recurse; `offsets` go straight to the leaf (they are always a primitive
212225
/// column); and `validity` uses the shared validity strategy.
213-
fn list_strategy(&self) -> ListLayoutStrategy {
214-
ListLayoutStrategy::default()
226+
fn list_strategy(&self) -> Option<Arc<dyn LayoutStrategy>> {
227+
let factory = self.list_layout_factory.as_ref()?;
228+
let list_layout = ListLayoutStrategy::default()
215229
.with_elements(Arc::new(self.descend_clean()))
216230
.with_offsets(Arc::clone(&self.leaf))
217231
.with_validity(Arc::clone(&self.validity))
218-
.with_fallback(Arc::clone(&self.leaf))
232+
.with_fallback(Arc::clone(&self.leaf));
233+
Some(factory(list_layout))
219234
}
220235

221236
/// Descend into a subfield, retaining only the overrides that apply beneath it (rebased to be
@@ -236,7 +251,7 @@ impl TableStrategy {
236251
leaf_writers: new_writers,
237252
validity: Arc::clone(&self.validity),
238253
leaf: Arc::clone(&self.leaf),
239-
use_list_layout: self.use_list_layout,
254+
list_layout_factory: self.list_layout_factory.clone(),
240255
}
241256
}
242257

@@ -247,7 +262,7 @@ impl TableStrategy {
247262
leaf_writers: HashMap::default(),
248263
validity: Arc::clone(&self.validity),
249264
leaf: Arc::clone(&self.leaf),
250-
use_list_layout: self.use_list_layout,
265+
list_layout_factory: self.list_layout_factory.clone(),
251266
}
252267
}
253268

@@ -290,9 +305,10 @@ impl LayoutStrategy for TableStrategy {
290305
.await;
291306
}
292307

293-
if dtype.is_list() && self.use_list_layout {
294-
return self
295-
.list_strategy()
308+
if dtype.is_list()
309+
&& let Some(list_strategy) = self.list_strategy()
310+
{
311+
return list_strategy
296312
.write_stream(ctx, segment_sink, stream, eof, session)
297313
.await;
298314
}
@@ -306,6 +322,7 @@ impl LayoutStrategy for TableStrategy {
306322

307323
#[cfg(test)]
308324
mod tests {
325+
use std::num::NonZeroUsize;
309326
use std::sync::Arc;
310327
use std::task::Poll;
311328

@@ -325,6 +342,7 @@ mod tests {
325342
use vortex_array::field_path;
326343
use vortex_array::validity::Validity;
327344
use vortex_buffer::buffer;
345+
use vortex_error::VortexExpect;
328346
use vortex_error::VortexResult;
329347
use vortex_io::runtime::single::block_on;
330348
use vortex_io::session::RuntimeSessionExt;
@@ -333,7 +351,13 @@ mod tests {
333351
use crate::LayoutStrategy;
334352
use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
335353
use crate::layouts::flat::writer::FlatLayoutStrategy;
354+
use crate::layouts::list::List;
355+
use crate::layouts::repartition::RepartitionStrategy;
356+
use crate::layouts::repartition::RepartitionWriterOptions;
336357
use crate::layouts::table::TableStrategy;
358+
use crate::layouts::zoned::Zoned;
359+
use crate::layouts::zoned::writer::ZonedLayoutOptions;
360+
use crate::layouts::zoned::writer::ZonedStrategy;
337361
use crate::segments::TestSegments;
338362
use crate::sequence::SequenceId;
339363
use crate::sequence::SequentialArrayStreamExt;
@@ -478,6 +502,54 @@ mod tests {
478502
Ok(())
479503
}
480504

505+
/// A wrapper can repartition and zone lists in outer-row space before decomposition.
506+
#[tokio::test]
507+
async fn wraps_list_strategy_before_decomposition() -> VortexResult<()> {
508+
let list = ListArray::try_new(
509+
PrimitiveArray::from_iter(0..9_i32).into_array(),
510+
PrimitiveArray::from_iter(0..=9_u32).into_array(),
511+
Validity::NonNullable,
512+
)?
513+
.into_array();
514+
515+
let row_block_size = NonZeroUsize::new(4).vortex_expect("4 is non-zero");
516+
let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
517+
let stats = Arc::clone(&flat);
518+
let chunked: Arc<dyn LayoutStrategy> =
519+
Arc::new(ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()));
520+
let dispatcher = TableStrategy::new(Arc::clone(&flat), chunked).with_list_layout_factory(
521+
move |list_layout| {
522+
let zoned = ZonedStrategy::new(
523+
list_layout,
524+
Arc::clone(&stats),
525+
ZonedLayoutOptions {
526+
block_size: row_block_size,
527+
..Default::default()
528+
},
529+
);
530+
Arc::new(RepartitionStrategy::new(
531+
zoned,
532+
RepartitionWriterOptions {
533+
block_size_minimum: 0,
534+
block_len_multiple: row_block_size.get(),
535+
block_size_target: None,
536+
canonicalize: false,
537+
},
538+
)) as Arc<dyn LayoutStrategy>
539+
},
540+
);
541+
542+
let layout = write(&dispatcher, list).await?;
543+
let zoned = layout.as_::<Zoned>();
544+
assert_eq!(zoned.zone_len(), 4);
545+
assert_eq!(zoned.nzones(), 3);
546+
547+
let data = layout.child(0)?;
548+
assert!(data.is::<List>());
549+
assert_eq!(data.row_count(), 9);
550+
Ok(())
551+
}
552+
481553
/// A non-struct stream is not shredded; it is handed straight to the leaf strategy.
482554
#[tokio::test]
483555
async fn non_struct_input_uses_leaf() -> VortexResult<()> {

0 commit comments

Comments
 (0)