-
Notifications
You must be signed in to change notification settings - Fork 215
Expand file tree
/
Copy pathstrategy.rs
More file actions
315 lines (284 loc) · 13.6 KB
/
Copy pathstrategy.rs
File metadata and controls
315 lines (284 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors
//! This module defines the default layout strategy for a Vortex file.
use std::num::NonZeroUsize;
use std::sync::Arc;
use vortex_array::ArrayId;
use vortex_array::dtype::FieldPath;
use vortex_btrblocks::BtrBlocksCompressorBuilder;
use vortex_btrblocks::SchemeExt;
use vortex_btrblocks::schemes::integer::IntDictScheme;
use vortex_error::VortexExpect;
use vortex_layout::LayoutStrategy;
use vortex_layout::LayoutStrategyEncodingValidator;
use vortex_layout::layouts::buffered::BufferedStrategy;
use vortex_layout::layouts::chunked::writer::ChunkedLayoutStrategy;
use vortex_layout::layouts::collect::CollectStrategy;
use vortex_layout::layouts::compressed::CompressingStrategy;
use vortex_layout::layouts::compressed::CompressorPlugin;
use vortex_layout::layouts::dict::writer::DictStrategy;
use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
use vortex_layout::layouts::list::writer::ListLayoutStrategy;
use vortex_layout::layouts::repartition::RepartitionStrategy;
use vortex_layout::layouts::repartition::RepartitionWriterOptions;
use vortex_layout::layouts::table::TableStrategy;
use vortex_layout::layouts::zoned::writer::ZonedLayoutOptions;
use vortex_layout::layouts::zoned::writer::ZonedStrategy;
use vortex_utils::aliases::hash_map::HashMap;
use vortex_utils::aliases::hash_set::HashSet;
const ONE_MEG: u64 = 1 << 20;
#[cfg(feature = "unstable_encodings")]
const USE_LIST_LAYOUT_BY_DEFAULT: bool = true;
#[cfg(not(feature = "unstable_encodings"))]
const USE_LIST_LAYOUT_BY_DEFAULT: bool = false;
/// How the compressor was configured on [`WriteStrategyBuilder`].
enum CompressorConfig {
/// A [`BtrBlocksCompressorBuilder`] that [`WriteStrategyBuilder::build`] will finalize.
/// `IntDictScheme` is automatically excluded from the data compressor to prevent recursive
/// dictionary encoding.
BtrBlocks(BtrBlocksCompressorBuilder),
/// An opaque compressor used as-is for both data and stats compression.
Opaque(Arc<dyn CompressorPlugin>),
}
/// Build a new [writer strategy](LayoutStrategy) to compress and reorganize chunks of a Vortex
/// file.
///
/// Vortex provides an out-of-the-box file writer that optimizes the layout of chunks on-disk,
/// repartitioning and compressing them to strike a balance between size on-disk,
/// bulk decoding performance, and IOPS required to perform an indexed read.
///
/// The default pipeline first splits struct columns, repartitions rows into fixed-size row blocks,
/// computes zoned statistics, applies dictionary encoding where useful, coalesces chunks toward
/// segment-sized blocks, compresses arrays, buffers nearby chunks, and finally writes flat leaf
/// layouts.
pub struct WriteStrategyBuilder {
compressor: CompressorConfig,
row_block_size: usize,
data_block_target_bytes: Option<u64>,
field_writers: HashMap<FieldPath, Arc<dyn LayoutStrategy>>,
allow_encodings: Option<HashSet<ArrayId>>,
flat_strategy: Option<Arc<dyn LayoutStrategy>>,
probe_compressor: Option<Arc<dyn CompressorPlugin>>,
/// Whether to write list fields using [`ListLayoutStrategy`].
/// Enabled by default with the `unstable_encodings` feature.
///
/// [`ListLayoutStrategy`]: vortex_layout::layouts::list::writer::ListLayoutStrategy
use_list_layout: bool,
}
impl Default for WriteStrategyBuilder {
/// Create a new empty builder. It can be further configured,
/// and then finally built yielding the [`LayoutStrategy`].
fn default() -> Self {
Self {
compressor: CompressorConfig::BtrBlocks(BtrBlocksCompressorBuilder::default()),
row_block_size: 8192,
data_block_target_bytes: Some(ONE_MEG),
field_writers: HashMap::new(),
allow_encodings: None,
flat_strategy: None,
probe_compressor: None,
use_list_layout: USE_LIST_LAYOUT_BY_DEFAULT,
}
}
}
impl WriteStrategyBuilder {
/// Override the row block size used for row repartitioning and zoned statistics.
///
/// Larger blocks reduce footer/statistics overhead. Smaller blocks can improve pruning and
/// random-access locality.
pub fn with_row_block_size(mut self, row_block_size: usize) -> Self {
self.row_block_size = row_block_size;
self
}
/// Override the target uncompressed byte size used to coalesce data blocks.
///
/// Passing `None` disables byte-size coalescing, so blocks retain the row granularity set by
/// [`Self::with_row_block_size`].
pub fn with_data_block_target_bytes(mut self, target_bytes: Option<u64>) -> Self {
self.data_block_target_bytes = target_bytes;
self
}
/// Enable writing list fields with [`ListLayoutStrategy`].
///
/// This is already enabled by default when the `unstable_encodings` feature is active.
///
/// **Note**: this is an unstable and experimental layout that is expected to change.
/// Using it may lead to unreadable files in the future.
///
/// [`ListLayoutStrategy`]: vortex_layout::layouts::list::writer::ListLayoutStrategy
pub fn with_list_layout(mut self) -> Self {
self.use_list_layout = true;
self
}
/// Override the write layout for a specific field somewhere in the nested schema tree.
///
/// The field path is matched after the root struct is split into columns. This is useful when a
/// column needs a custom compression/layout policy while the rest of the file uses defaults.
pub fn with_field_writer(
mut self,
field: impl Into<FieldPath>,
writer: Arc<dyn LayoutStrategy>,
) -> Self {
self.field_writers.insert(field.into(), writer);
self
}
/// Override the allowed array encodings for file writing.
///
/// The configured flat leaf strategy is wrapped in a [`LayoutStrategyEncodingValidator`]
/// that recursively checks every chunk before passing it to the leaf writer. [`build`](Self::build)
/// also restricts any [`BtrBlocksCompressorBuilder`] to these encodings, independent of the
/// order in which the builder and this policy were configured.
pub fn with_allow_encodings(mut self, allow_encodings: HashSet<ArrayId>) -> Self {
self.allow_encodings = Some(allow_encodings);
self
}
/// Override the flat layout strategy used for leaf chunks.
///
/// By default, this uses [`FlatLayoutStrategy`]. This can be used to substitute a custom
/// layout strategy, e.g. one that inlines constant array buffers for GPU reads.
pub fn with_flat_strategy(mut self, flat: Arc<dyn LayoutStrategy>) -> Self {
self.flat_strategy = Some(flat);
self
}
/// Override the default [`BtrBlocksCompressorBuilder`] used for compression.
///
/// The builder is finalized during [`build`](Self::build), producing two compressors: one for
/// data (with `IntDictScheme` excluded) and one for stats. Both are restricted to the
/// configured allowed encodings at build time.
pub fn with_btrblocks_builder(mut self, builder: BtrBlocksCompressorBuilder) -> Self {
self.compressor = CompressorConfig::BtrBlocks(builder);
self
}
/// Set the compressor to an opaque [`CompressorPlugin`].
///
/// The compressor is used as-is for both data and stats compression. Use this when the
/// compressor is already fully configured and should not be modified by the builder.
pub fn with_compressor<C: CompressorPlugin>(mut self, compressor: C) -> Self {
self.compressor = CompressorConfig::Opaque(Arc::new(compressor));
self
}
/// Override the compressor used to probe whether a column is dict-eligible.
pub fn with_probe_compressor<C: CompressorPlugin>(mut self, compressor: C) -> Self {
self.probe_compressor = Some(Arc::new(compressor));
self
}
/// Builds the canonical [`LayoutStrategy`] implementation, with the configured overrides
/// applied.
pub fn build(self) -> Arc<dyn LayoutStrategy> {
let flat: Arc<dyn LayoutStrategy> = if let Some(flat) = self.flat_strategy {
flat
} else {
Arc::new(FlatLayoutStrategy::default())
};
// Restrict the compressor to the allowed encodings so no compressor derived below (data,
// stats, and the defaulted probe compressor) can produce an encoding outside the policy,
// regardless of the order in which the builder and the policy were configured.
let compressor = match self.compressor {
CompressorConfig::BtrBlocks(builder) => {
CompressorConfig::BtrBlocks(match &self.allow_encodings {
Some(allow_encodings) => builder.retain_allowed_encodings(allow_encodings),
None => builder,
})
}
opaque => opaque,
};
let flat: Arc<dyn LayoutStrategy> = if let Some(allow_encodings) = self.allow_encodings {
Arc::new(LayoutStrategyEncodingValidator::new(flat, allow_encodings))
} else {
flat
};
// 7. write each compressed chunk as either a shallow list layout or a flat layout.
// Chunking stays outside ListLayoutStrategy so every list layout receives exactly one
// complete page and its elements, offsets, and validity are direct flat leaves.
let terminal: Arc<dyn LayoutStrategy> = if self.use_list_layout {
Arc::new(
ListLayoutStrategy::default()
.with_children_strategy(Arc::clone(&flat))
.with_fallback(Arc::clone(&flat)),
)
} else {
Arc::clone(&flat)
};
let chunked = ChunkedLayoutStrategy::new(terminal);
// 6. buffer chunks so they end up with closer segment ids physically
let buffered = BufferedStrategy::new(chunked, 2 * ONE_MEG); // 2MB
// 5. compress each chunk.
// Exclude IntDictScheme from the data compressor because DictStrategy (step 3) already
// dictionary-encodes columns. Allowing IntDictScheme here would redundantly
// dictionary-encode the integer codes produced by that earlier step.
let data_compressor: Arc<dyn CompressorPlugin> = match &compressor {
CompressorConfig::BtrBlocks(builder) => Arc::new(
builder
.clone()
.exclude_schemes([IntDictScheme.id()])
.build(),
),
CompressorConfig::Opaque(compressor) => Arc::clone(compressor),
};
let compressing = CompressingStrategy::new(buffered, data_compressor);
// 4. prior to compression, coalesce up to a minimum size
let coalescing = RepartitionStrategy::new(
compressing,
RepartitionWriterOptions {
// Write stream partitions roughly become segments. Because Vortex never reads less
// than one segment, the size of segments and, therefore, partitions, must be small
// enough to both (1) allow fine-grained random access reads and (2) allow
// sufficient read concurrency for the desired throughput. One megabyte is small
// enough to achieve this for S3 (Durner et al., "Exploiting Cloud Object Storage for
// High-Performance Analytics", VLDB Vol 16, Iss 11).
block_size_minimum: self.data_block_target_bytes.unwrap_or(0),
block_len_multiple: self.row_block_size,
block_size_target: self.data_block_target_bytes,
canonicalize: true,
},
);
// 2.1. | 3.1. compress stats tables and dict values.
let stats_compressor: Arc<dyn CompressorPlugin> = match compressor {
CompressorConfig::BtrBlocks(builder) => Arc::new(builder.build()),
CompressorConfig::Opaque(compressor) => compressor,
};
let compress_then_flat =
CompressingStrategy::new(Arc::clone(&flat), Arc::clone(&stats_compressor));
// 3. apply dict encoding or fallback
let probe_compressor = if let Some(probe_compressor) = self.probe_compressor {
probe_compressor
} else {
Arc::clone(&stats_compressor)
};
let dict = DictStrategy::new(
coalescing.clone(),
compress_then_flat.clone(),
coalescing,
Default::default(),
probe_compressor,
);
let row_block_size = NonZeroUsize::new(self.row_block_size).vortex_expect("must be non 0");
// 2. calculate stats for each row group
let stats = ZonedStrategy::new(
dict,
compress_then_flat.clone(),
ZonedLayoutOptions {
block_size: row_block_size,
..Default::default()
},
);
// 1. repartition each column to fixed row counts
let repartition = RepartitionStrategy::new(
stats,
RepartitionWriterOptions {
// No minimum block size in bytes
block_size_minimum: 0,
// Always repartition into 8K row blocks
block_len_multiple: self.row_block_size,
block_size_target: None,
canonicalize: false,
},
);
// 0. start with splitting columns
let validity_strategy = CollectStrategy::new(compress_then_flat);
// Take any field overrides from the builder and apply them to the final strategy.
let table_strategy = TableStrategy::new(Arc::new(validity_strategy), Arc::new(repartition))
.with_field_writers(self.field_writers);
Arc::new(table_strategy)
}
}