vortex_array/arrays/varbinview/
compact.rs1use std::ops::Range;
8
9use vortex_error::VortexExpect;
10use vortex_error::VortexResult;
11use vortex_mask::Mask;
12
13use crate::ExecutionCtx;
14use crate::arrays::VarBinViewArray;
15use crate::arrays::varbinview::Ref;
16use crate::builders::VarBinViewBuilder;
17
18const DEFAULT_COMPACTION_THRESHOLD: f64 = 0.5;
19const MIN_RETAINED_BYTES_PER_ROW_TO_CHECK_COMPACTION: u64 = 128;
20
21impl VarBinViewArray {
22 pub fn compact_buffers(&self, ctx: &mut ExecutionCtx) -> VortexResult<VarBinViewArray> {
31 if !self.should_compact(ctx)? {
33 return Ok(self.clone());
34 }
35
36 self.compact_with_threshold(DEFAULT_COMPACTION_THRESHOLD, ctx)
37 }
38
39 fn should_compact(&self, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
40 let nbuffers = self.data_buffers().len();
41
42 if nbuffers == 0 {
44 return Ok(false);
45 }
46
47 if nbuffers > u16::MAX as usize {
49 return Ok(true);
50 }
51
52 let buffer_total_bytes: u64 = self.buffers.iter().map(|buf| buf.len() as u64).sum();
53 if buffer_total_bytes == 0 {
54 return Ok(true);
55 }
56
57 let len = u64::try_from(self.len()).unwrap_or(u64::MAX);
58 if len > 0 && buffer_total_bytes / len <= MIN_RETAINED_BYTES_PER_ROW_TO_CHECK_COMPACTION {
59 return Ok(false);
60 }
61
62 let bytes_referenced: u64 = self.count_referenced_bytes(ctx)?;
63 Ok((bytes_referenced as f64 / buffer_total_bytes as f64) < DEFAULT_COMPACTION_THRESHOLD)
64 }
65
66 #[allow(clippy::inline_always)]
69 #[inline(always)]
70 fn iter_valid_views<F>(&self, ctx: &mut ExecutionCtx, mut f: F) -> VortexResult<()>
71 where
72 F: FnMut(&Ref),
73 {
74 match self
75 .as_ref()
76 .validity()?
77 .execute_mask(self.as_ref().len(), ctx)?
78 {
79 Mask::AllTrue(_) => {
80 for &view in self.views().iter() {
81 if !view.is_inlined() {
82 f(view.as_view());
83 }
84 }
85 }
86 Mask::AllFalse(_) => {}
87 Mask::Values(v) => {
88 for (&view, is_valid) in self.views().iter().zip(v.bit_buffer().iter()) {
89 if is_valid && !view.is_inlined() {
90 f(view.as_view());
91 }
92 }
93 }
94 }
95 Ok(())
96 }
97
98 fn count_referenced_bytes(&self, ctx: &mut ExecutionCtx) -> VortexResult<u64> {
101 let mut total = 0u64;
102 self.iter_valid_views(ctx, |view| total += view.size as u64)?;
103 Ok(total)
104 }
105
106 pub(crate) fn buffer_utilizations(
107 &self,
108 ctx: &mut ExecutionCtx,
109 ) -> VortexResult<Vec<BufferUtilization>> {
110 let mut utilizations: Vec<BufferUtilization> = self
111 .data_buffers()
112 .iter()
113 .map(|buf| {
114 let len = u32::try_from(buf.len()).vortex_expect("buffer sizes must fit in u32");
115 BufferUtilization::zero(len)
116 })
117 .collect();
118
119 self.iter_valid_views(ctx, |view| {
120 utilizations[view.buffer_index as usize].add(view.offset, view.size);
121 })?;
122
123 Ok(utilizations)
124 }
125
126 pub fn compact_with_threshold(
141 &self,
142 buffer_utilization_threshold: f64, ctx: &mut ExecutionCtx,
144 ) -> VortexResult<VarBinViewArray> {
145 let mut builder = VarBinViewBuilder::with_compaction_in(
146 self.dtype().clone(),
147 self.len(),
148 buffer_utilization_threshold,
149 ctx.allocator().clone(),
150 );
151 builder.append_varbinview_array(self, ctx)?;
152 Ok(builder.finish_into_varbinview())
153 }
154}
155
156pub(crate) struct BufferUtilization {
157 len: u32,
158 used: u32,
159 min_offset: u32,
160 max_offset_end: u32,
161}
162
163impl BufferUtilization {
164 pub(crate) fn zero(len: u32) -> Self {
165 BufferUtilization {
166 len,
167 used: 0u32,
168 min_offset: u32::MAX,
169 max_offset_end: 0,
170 }
171 }
172
173 pub(crate) fn add(&mut self, offset: u32, size: u32) {
174 self.used += size;
175 self.min_offset = self.min_offset.min(offset);
176 self.max_offset_end = self.max_offset_end.max(offset + size);
177 }
178
179 pub fn overall_utilization(&self) -> f64 {
180 match self.len {
181 0 => 0.0,
182 len => self.used as f64 / len as f64,
183 }
184 }
185
186 pub fn range_utilization(&self) -> f64 {
187 match self.range_span() {
188 0 => 0.0,
189 span => self.used as f64 / span as f64,
190 }
191 }
192
193 pub fn range(&self) -> Range<u32> {
194 self.min_offset..self.max_offset_end
195 }
196
197 fn range_span(&self) -> u32 {
198 self.max_offset_end.saturating_sub(self.min_offset)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use rstest::rstest;
205 use vortex_buffer::buffer;
206
207 use crate::IntoArray;
208 use crate::VortexSessionExecute;
209 use crate::array_session;
210 use crate::arrays::VarBinArray;
211 use crate::arrays::VarBinViewArray;
212 use crate::assert_arrays_eq;
213 use crate::dtype::DType;
214 use crate::dtype::Nullability;
215 #[test]
216 fn test_optimize_compacts_buffers() {
217 let mut ctx = array_session().create_execution_ctx();
218 let original = VarBinViewArray::from_iter_nullable_str([
220 Some("short"),
221 Some("this is a longer string that will be stored in a buffer"),
222 Some("medium length string"),
223 Some("another very long string that definitely needs a buffer to store it"),
224 Some("tiny"),
225 ]);
226
227 assert!(!original.data_buffers().is_empty());
229 let original_buffers = original.data_buffers().len();
230
231 let indices = buffer![0u32, 4u32].into_array();
233 let taken = original.take(indices).unwrap();
234 let taken = taken.execute::<VarBinViewArray>(&mut ctx).unwrap();
235 assert_eq!(taken.data_buffers().len(), original_buffers);
237
238 let optimized_array = taken.compact_buffers(&mut ctx).unwrap();
240
241 assert!(optimized_array.data_buffers().len() <= 1);
245
246 assert_arrays_eq!(
248 optimized_array,
249 <VarBinArray as FromIterator<_>>::from_iter([Some("short"), Some("tiny")]),
250 &mut ctx
251 );
252 }
253
254 #[test]
255 fn test_optimize_with_long_strings() {
256 let mut ctx = array_session().create_execution_ctx();
257 let long_string_1 = "this is definitely a very long string that exceeds the inline limit";
259 let long_string_2 = "another extremely long string that also needs external buffer storage";
260 let long_string_3 = "yet another long string for testing buffer compaction functionality";
261
262 let original = VarBinViewArray::from_iter_str([
263 long_string_1,
264 long_string_2,
265 long_string_3,
266 "short1",
267 "short2",
268 ]);
269
270 let indices = buffer![0u32, 2u32].into_array();
272 let taken = original.take(indices).unwrap();
273 let taken_array = taken
274 .execute::<VarBinViewArray>(&mut array_session().create_execution_ctx())
275 .unwrap();
276
277 let optimized_array = taken_array.compact_with_threshold(1.0, &mut ctx).unwrap();
278
279 assert_eq!(optimized_array.data_buffers().len(), 1);
281
282 assert_arrays_eq!(
284 optimized_array,
285 VarBinArray::from(vec![long_string_1, long_string_3]),
286 &mut ctx
287 );
288 }
289
290 #[test]
291 fn test_optimize_no_buffers() {
292 let mut ctx = array_session().create_execution_ctx();
293 let original = VarBinViewArray::from_iter_str(["a", "bb", "ccc", "dddd"]);
295
296 assert_eq!(original.data_buffers().len(), 0);
298
299 let optimized_array = original.compact_buffers(&mut ctx).unwrap();
301
302 assert_eq!(optimized_array.data_buffers().len(), 0);
303
304 assert_arrays_eq!(optimized_array, original, &mut ctx);
305 }
306
307 #[test]
308 fn test_optimize_single_buffer() {
309 let mut ctx = array_session().create_execution_ctx();
310 let str1 = "this is a long string that goes into a buffer";
312 let str2 = "another long string in the same buffer";
313 let original = VarBinViewArray::from_iter_str([str1, str2]);
314
315 assert_eq!(original.data_buffers().len(), 1);
317 assert_eq!(original.buffer(0).len(), str1.len() + str2.len());
318
319 let optimized_array = original.compact_buffers(&mut ctx).unwrap();
321
322 assert_eq!(optimized_array.data_buffers().len(), 1);
323
324 assert_arrays_eq!(optimized_array, original, &mut ctx);
325 }
326
327 #[test]
328 fn test_selective_compaction_with_threshold_zero() {
329 let mut ctx = array_session().create_execution_ctx();
330 let original = VarBinViewArray::from_iter_str([
332 "this is a longer string that will be stored in a buffer",
333 "another very long string that definitely needs a buffer to store it",
334 ]);
335
336 let original_buffers = original.data_buffers().len();
337 assert!(original_buffers > 0);
338
339 let indices = buffer![0u32].into_array();
341 let taken = original.take(indices).unwrap();
342 let taken = taken
343 .execute::<VarBinViewArray>(&mut array_session().create_execution_ctx())
344 .unwrap();
345 let compacted = taken.compact_with_threshold(0.0, &mut ctx).unwrap();
347
348 assert_eq!(compacted.data_buffers().len(), taken.data_buffers().len());
350
351 assert_arrays_eq!(compacted, taken, &mut ctx);
353 }
354
355 #[test]
356 fn test_selective_compaction_with_high_threshold() {
357 let mut ctx = array_session().create_execution_ctx();
358 let original = VarBinViewArray::from_iter_str([
360 "this is a longer string that will be stored in a buffer",
361 "another very long string that definitely needs a buffer to store it",
362 "yet another long string",
363 ]);
364
365 let indices = buffer![0u32, 2u32].into_array();
367 let taken = original.take(indices).unwrap();
368 let taken = taken
369 .execute::<VarBinViewArray>(&mut array_session().create_execution_ctx())
370 .unwrap();
371
372 let original_buffers = taken.data_buffers().len();
373
374 let compacted = taken.compact_with_threshold(1.0, &mut ctx).unwrap();
376
377 assert!(compacted.data_buffers().len() <= original_buffers);
379
380 assert_arrays_eq!(compacted, taken, &mut ctx);
382 }
383
384 #[test]
385 fn test_selective_compaction_preserves_well_utilized_buffers() {
386 let mut ctx = array_session().create_execution_ctx();
387 let str1 = "first long string that needs external buffer storage";
389 let str2 = "second long string also in buffer";
390 let str3 = "third long string in same buffer";
391
392 let original = VarBinViewArray::from_iter_str([str1, str2, str3]);
393
394 assert_eq!(original.data_buffers().len(), 1);
396
397 let compacted = original.compact_with_threshold(0.8, &mut ctx).unwrap();
399
400 assert_eq!(compacted.data_buffers().len(), 1);
402
403 assert_arrays_eq!(compacted, original, &mut ctx);
405 }
406
407 #[test]
408 fn test_selective_compaction_with_mixed_utilization() {
409 let mut ctx = array_session().create_execution_ctx();
410 let strings: Vec<String> = (0..10)
412 .map(|i| {
413 format!(
414 "this is a long string number {} that needs buffer storage",
415 i
416 )
417 })
418 .collect();
419
420 let original = VarBinViewArray::from_iter_str(strings.iter().map(|s| s.as_str()));
421
422 let indices_array = buffer![0u32, 2u32, 4u32, 6u32, 8u32].into_array();
424 let taken = original.take(indices_array).unwrap();
425 let taken = taken
426 .execute::<VarBinViewArray>(&mut array_session().create_execution_ctx())
427 .unwrap();
428
429 let compacted = taken.compact_with_threshold(0.7, &mut ctx).unwrap();
431
432 let expected = VarBinViewArray::from_iter(
433 [0, 2, 4, 6, 8].map(|i| Some(strings[i].as_str())),
434 DType::Utf8(Nullability::NonNullable),
435 );
436 assert_arrays_eq!(expected, compacted, &mut ctx);
437 }
438
439 #[test]
440 fn test_slice_strategy_with_contiguous_range() {
441 let mut ctx = array_session().create_execution_ctx();
442 let strings: Vec<String> = (0..20)
444 .map(|i| format!("this is a long string number {} for slice test", i))
445 .collect();
446
447 let original = VarBinViewArray::from_iter_str(strings.iter().map(|s| s.as_str()));
448
449 let indices_array = buffer![0u32, 1u32, 2u32, 3u32, 4u32].into_array();
451 let taken = original.take(indices_array).unwrap();
452 let taken = taken
453 .execute::<VarBinViewArray>(&mut array_session().create_execution_ctx())
454 .unwrap();
455 let utils_before = taken.buffer_utilizations(&mut ctx).unwrap();
457 let original_buffer_count = taken.data_buffers().len();
458
459 let compacted = taken.compact_with_threshold(0.8, &mut ctx).unwrap();
462
463 assert!(
465 !compacted.data_buffers().is_empty(),
466 "Should have buffers after slice compaction"
467 );
468
469 assert_arrays_eq!(&compacted, taken, &mut ctx);
471
472 if original_buffer_count == 1 && utils_before[0].range_utilization() >= 0.8 {
475 assert_eq!(
476 compacted.data_buffers().len(),
477 1,
478 "Slice strategy should maintain single buffer"
479 );
480 }
481 }
482
483 const LONG1: &str = "long string one!";
484 const LONG2: &str = "long string two!";
485 const SHORT: &str = "x";
486 const EXPECTED_BYTES: u64 = (LONG1.len() + LONG2.len()) as u64;
487
488 fn mixed_array() -> VarBinViewArray {
489 VarBinViewArray::from_iter_nullable_str([Some(LONG1), None, Some(LONG2), Some(SHORT)])
490 }
491
492 #[rstest]
493 #[case::non_nullable(VarBinViewArray::from_iter_str([LONG1, LONG2, SHORT]), EXPECTED_BYTES, &[1.0])]
494 #[case::all_valid(VarBinViewArray::from_iter_nullable_str([Some(LONG1), Some(LONG2), Some(SHORT)]), EXPECTED_BYTES, &[1.0])]
495 #[case::all_invalid(VarBinViewArray::from_iter_nullable_str([None::<&str>, None]), 0, &[])]
496 #[case::mixed_validity(mixed_array(), EXPECTED_BYTES, &[1.0])]
497 fn test_validity_code_paths(
498 #[case] arr: VarBinViewArray,
499 #[case] expected_bytes: u64,
500 #[case] expected_utils: &[f64],
501 ) {
502 let mut ctx = array_session().create_execution_ctx();
503 assert_eq!(
504 arr.count_referenced_bytes(&mut ctx).unwrap(),
505 expected_bytes
506 );
507 let utils: Vec<f64> = arr
508 .buffer_utilizations(&mut ctx)
509 .unwrap()
510 .iter()
511 .map(|u| u.overall_utilization())
512 .collect();
513 assert_eq!(utils, expected_utils);
514 }
515}