This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new c7be84a18c perf: Reduce record batch memory accounting overhead 
(#24319)
c7be84a18c is described below

commit c7be84a18c77b974fafc26f2cf7265526af4d18d
Author: Ryu <[email protected]>
AuthorDate: Mon Aug 17 19:29:25 2026 +0000

    perf: Reduce record batch memory accounting overhead (#24319)
    
    ## Which issue does this PR close?
    
    - Closes #24310.
    
    ## Rationale for this change
    
    Record batch memory accounting runs on hot execution paths. The current
    implementation materializes ArrayData for every array and allocates a
    hash set even for small batches, adding measurable overhead to queries
    that frequently update memory reservations.
    
    ## What changes are included in this PR?
    
    - Traverse Arrow arrays directly and recursively count their backing
    buffers without materializing ArrayData.
    - Track the first 16 buffer identities inline, then promote to a hash
    set for wider batches or counters spanning many batches.
    - Preserve shared-buffer deduplication and full buffer-capacity
    accounting semantics.
    - Add parity coverage against the previous ArrayData traversal for
    primitive, binary/view, list/view, fixed-size, struct, union,
    dictionary, map, and all legal run-end index layouts.
    - Add a focused Criterion benchmark across column counts, row counts,
    and primitive/list/struct layouts.
    
    Criterion point estimates from `cargo bench -p datafusion-common --bench
    record_batch_memory`, measured sequentially on upstream `main` and this
    PR on the same machine:
    
    ### Column count (8,192 rows, Int64)
    
    | Columns | main | this PR | Speedup |
    |---:|---:|---:|---:|
    | 1 | 81.999 ns | 10.645 ns | 7.70x |
    | 4 | 308.11 ns | 38.669 ns | 7.97x |
    | 16 | 1.2543 us | 173.55 ns | 7.23x |
    | 64 | 4.7457 us | 1.3914 us | 3.41x |
    
    ### Row count (4 Int64 columns)
    
    | Rows | main | this PR | Speedup |
    |---:|---:|---:|---:|
    | 1 | 299.20 ns | 39.939 ns | 7.49x |
    | 128 | 302.99 ns | 38.914 ns | 7.79x |
    | 8,192 | 307.64 ns | 39.740 ns | 7.74x |
    | 65,536 | 310.61 ns | 38.994 ns | 7.97x |
    
    ### Array layout (4 columns, 8,192 rows)
    
    | Layout | main | this PR | Speedup |
    |---|---:|---:|---:|
    | Primitive Int64 | 311.01 ns | 40.140 ns | 7.75x |
    | List of Int64 | 778.29 ns | 82.865 ns | 9.39x |
    | Struct of two Int64 fields | 1.0072 us | 113.70 ns | 8.86x |
    
    Each list row contains two Int64 values. Each struct column contains two
    Int64 child fields.
    
    ## Are these changes tested?
    
    Yes.
    
    - `cargo fmt --all -- --check`
    - `cargo clippy --all-targets --all-features -- -D warnings`
    - `cargo test -p datafusion-common utils::memory --lib` (10 passed)
    - `RUST_BACKTRACE=1 cargo test --profile ci --exclude
    datafusion-examples --exclude datafusion-benchmarks --exclude
    datafusion-cli --workspace --lib --tests --bins --features
    avro,json,backtrace,extended_tests,recursive_protection,parquet_encryption`
    - Focused Criterion comparison shown above
    
    ## Are there any user-facing changes?
    
    No API or behavior changes. This reduces CPU and allocation overhead in
    record batch memory accounting.
    
    AI assistance: OpenAI Codex assisted with implementation and test
    execution. I reviewed the change and its behavior end to end.
---
 datafusion/common/Cargo.toml                     |   4 +
 datafusion/common/benches/record_batch_memory.rs | 190 +++++++++++
 datafusion/common/src/utils/memory.rs            | 415 +++++++++++++++++++++--
 3 files changed, 577 insertions(+), 32 deletions(-)

diff --git a/datafusion/common/Cargo.toml b/datafusion/common/Cargo.toml
index 1eb23089a4..9ee199fe82 100644
--- a/datafusion/common/Cargo.toml
+++ b/datafusion/common/Cargo.toml
@@ -64,6 +64,10 @@ name = "scalar_to_array"
 harness = false
 name = "stats_merge"
 
+[[bench]]
+harness = false
+name = "record_batch_memory"
+
 [dependencies]
 arrow = { workspace = true }
 arrow-ipc = { workspace = true }
diff --git a/datafusion/common/benches/record_batch_memory.rs 
b/datafusion/common/benches/record_batch_memory.rs
new file mode 100644
index 0000000000..2479d6ac98
--- /dev/null
+++ b/datafusion/common/benches/record_batch_memory.rs
@@ -0,0 +1,190 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Measures the CPU overhead of accounting for the backing buffers retained by
+//! [`RecordBatch`]es. Batch construction is intentionally outside the timed
+//! region so the benchmarks isolate buffer traversal and identity 
deduplication.
+
+use std::hint::black_box;
+use std::sync::Arc;
+
+use arrow::array::{ArrayRef, Int64Array, ListArray, StructArray};
+use arrow::datatypes::{DataType, Field, Int64Type, Schema};
+use arrow::record_batch::RecordBatch;
+use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
+use datafusion_common::utils::memory::{
+    RecordBatchMemoryCounter, get_record_batch_memory_size,
+};
+
+fn make_batch(columns: Vec<ArrayRef>) -> RecordBatch {
+    let fields = columns
+        .iter()
+        .enumerate()
+        .map(|(index, column)| {
+            Field::new(format!("col_{index}"), column.data_type().clone(), 
false)
+        })
+        .collect::<Vec<_>>();
+
+    RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).unwrap()
+}
+
+fn make_primitive_batch(num_rows: usize, num_columns: usize) -> RecordBatch {
+    let columns = (0..num_columns)
+        .map(|index| {
+            Arc::new(Int64Array::from_iter_values(
+                (0..num_rows).map(|value| value as i64 + index as i64),
+            )) as ArrayRef
+        })
+        .collect::<Vec<_>>();
+
+    make_batch(columns)
+}
+
+fn make_list_batch(num_rows: usize, num_columns: usize) -> RecordBatch {
+    let columns = (0..num_columns)
+        .map(|column| {
+            Arc::new(ListArray::from_iter_primitive::<Int64Type, _, _>(
+                (0..num_rows).map(|row| {
+                    let value = row as i64 + column as i64;
+                    Some(vec![Some(value), Some(value + 1)])
+                }),
+            )) as ArrayRef
+        })
+        .collect::<Vec<_>>();
+
+    make_batch(columns)
+}
+
+fn make_struct_batch(num_rows: usize, num_columns: usize) -> RecordBatch {
+    let columns = (0..num_columns)
+        .map(|column| {
+            let left = Arc::new(Int64Array::from_iter_values(
+                (0..num_rows).map(|row| row as i64 + column as i64),
+            )) as ArrayRef;
+            let right = Arc::new(Int64Array::from_iter_values(
+                (0..num_rows).map(|row| row as i64 - column as i64),
+            )) as ArrayRef;
+
+            Arc::new(StructArray::from(vec![
+                (Arc::new(Field::new("left", DataType::Int64, false)), left),
+                (Arc::new(Field::new("right", DataType::Int64, false)), right),
+            ])) as ArrayRef
+        })
+        .collect::<Vec<_>>();
+
+    make_batch(columns)
+}
+
+fn benchmark_column_count(c: &mut Criterion) {
+    let mut group = c.benchmark_group("record_batch_memory_size/column_count");
+
+    // Each primitive column contributes a distinct backing buffer, exercising
+    // both the inline buffer-ID path and hash-set promotion.
+    for num_columns in [1, 4, 16, 64] {
+        let batch = make_primitive_batch(8192, num_columns);
+        group.bench_with_input(
+            BenchmarkId::from_parameter(num_columns),
+            &batch,
+            |bencher, batch| {
+                bencher.iter(|| 
get_record_batch_memory_size(black_box(batch)));
+            },
+        );
+    }
+
+    group.finish();
+}
+
+fn benchmark_row_count(c: &mut Criterion) {
+    let mut group = c.benchmark_group("record_batch_memory_size/row_count");
+
+    // Buffer traversal should depend on the number of buffers, not the number
+    // of values stored in each buffer.
+    for num_rows in [1, 128, 8192, 65_536] {
+        let batch = make_primitive_batch(num_rows, 4);
+        group.bench_with_input(
+            BenchmarkId::from_parameter(num_rows),
+            &batch,
+            |bencher, batch| {
+                bencher.iter(|| 
get_record_batch_memory_size(black_box(batch)));
+            },
+        );
+    }
+
+    group.finish();
+}
+
+fn benchmark_array_layout(c: &mut Criterion) {
+    let mut group = c.benchmark_group("record_batch_memory_size/array_layout");
+
+    // Compare direct primitive-buffer accounting with recursive traversal of
+    // representative nested layouts.
+    for (name, batch) in [
+        ("primitive", make_primitive_batch(8192, 4)),
+        ("list", make_list_batch(8192, 4)),
+        ("struct", make_struct_batch(8192, 4)),
+    ] {
+        group.bench_with_input(
+            BenchmarkId::from_parameter(name),
+            &batch,
+            |bencher, batch| {
+                bencher.iter(|| 
get_record_batch_memory_size(black_box(batch)));
+            },
+        );
+    }
+
+    group.finish();
+}
+
+fn benchmark_shared_slices(c: &mut Criterion) {
+    let mut group = 
c.benchmark_group("record_batch_memory_size/shared_slices");
+
+    // Model the hash-join build-side workload: one counter is reused across a
+    // sequence of zero-copy batch slices that retain the same backing buffers.
+    // Slicing happens outside the timed region; the benchmark measures 
repeated
+    // identity lookups and the one-time accounting of each shared buffer.
+    for num_columns in [4, 16, 64] {
+        let batch = make_primitive_batch(8192, num_columns);
+        let slices = (0..32)
+            .map(|index| batch.slice(index * 256, 256))
+            .collect::<Vec<_>>();
+
+        group.bench_with_input(
+            BenchmarkId::from_parameter(num_columns),
+            &slices,
+            |bencher, slices| {
+                bencher.iter(|| {
+                    let mut counter = RecordBatchMemoryCounter::new();
+                    for batch in black_box(slices) {
+                        black_box(counter.count_batch(black_box(batch)));
+                    }
+                    black_box(counter.memory_usage())
+                });
+            },
+        );
+    }
+
+    group.finish();
+}
+
+criterion_group!(
+    benches,
+    benchmark_column_count,
+    benchmark_row_count,
+    benchmark_array_layout,
+    benchmark_shared_slices
+);
+criterion_main!(benches);
diff --git a/datafusion/common/src/utils/memory.rs 
b/datafusion/common/src/utils/memory.rs
index 21c084119e..fd405e06a2 100644
--- a/datafusion/common/src/utils/memory.rs
+++ b/datafusion/common/src/utils/memory.rs
@@ -19,11 +19,24 @@
 
 use crate::error::_exec_datafusion_err;
 use crate::{HashSet, Result};
-use arrow::array::ArrayData;
+use arrow::array::types::{ByteArrayType, ByteViewType, RunEndIndexType};
+use arrow::array::{
+    Array, AsArray, GenericByteArray, GenericByteViewArray, GenericListArray,
+    GenericListViewArray, RunArray,
+};
+use arrow::buffer::Buffer;
+use arrow::datatypes::DataType;
+use arrow::downcast_primitive_array;
 use arrow::record_batch::RecordBatch;
 use std::mem::size_of;
 use std::num::NonZero;
 
+/// Maximum number of distinct buffer IDs retained inline before promotion to
+/// a [`HashSet`]. Sixteen keeps small buffer sets allocation-free while
+/// limiting linear lookup and inline storage to 16 pointer-sized entries.
+/// This is a performance heuristic, not a semantic limit.
+const INLINE_BUFFER_IDS: usize = 16;
+
 /// Estimates the memory size required for a hash table prior to allocation.
 ///
 /// # Parameters
@@ -151,7 +164,7 @@ pub fn get_record_batch_memory_size(batch: &RecordBatch) -> 
usize {
 pub struct RecordBatchMemoryCounter {
     /// Start addresses of `Buffer`s that have already been counted (instead of
     /// actual used data region's pointer represented by current `Array`)
-    counted_buffers: HashSet<NonZero<usize>>,
+    counted_buffers: BufferIdSet,
     /// Total memory of all unique buffers counted so far
     memory_usage: usize,
 }
@@ -164,49 +177,229 @@ impl RecordBatchMemoryCounter {
     /// Count `batch`, returning the memory used by its buffers that have not
     /// been counted before.
     pub fn count_batch(&mut self, batch: &RecordBatch) -> usize {
-        let mut total_size = 0;
+        let previous_memory_usage = self.memory_usage;
 
         for array in batch.columns() {
-            let array_data = array.to_data();
-            count_array_data_memory_size(
-                &array_data,
-                &mut self.counted_buffers,
-                &mut total_size,
-            );
+            self.count_array_memory_size(array.as_ref());
         }
 
-        self.memory_usage += total_size;
-        total_size
+        self.memory_usage - previous_memory_usage
     }
 
     /// Total memory of the unique buffers of all batches counted so far.
     pub fn memory_usage(&self) -> usize {
         self.memory_usage
     }
-}
 
-/// Count the memory usage of `array_data` and its children recursively.
-fn count_array_data_memory_size(
-    array_data: &ArrayData,
-    counted_buffers: &mut HashSet<NonZero<usize>>,
-    total_size: &mut usize,
-) {
-    // Count memory usage for `array_data`
-    for buffer in array_data.buffers() {
-        if counted_buffers.insert(buffer.data_ptr().addr()) {
-            *total_size += buffer.capacity();
-        } // Otherwise the buffer's memory is already counted
+    fn count_buffer_memory_size(&mut self, buffer: &Buffer) {
+        if self.counted_buffers.insert(buffer.data_ptr().addr()) {
+            self.memory_usage += buffer.capacity();
+        }
     }
 
-    if let Some(null_buffer) = array_data.nulls()
-        && 
counted_buffers.insert(null_buffer.inner().inner().data_ptr().addr())
-    {
-        *total_size += null_buffer.inner().inner().capacity();
+    /// Count the memory usage of `array` and its children recursively.
+    fn count_array_memory_size(&mut self, array: &dyn Array) {
+        if let Some(nulls) = array.nulls() {
+            self.count_buffer_memory_size(nulls.buffer());
+        }
+
+        downcast_primitive_array! {
+            array => self.count_buffer_memory_size(array.values().inner()),
+            DataType::Null => {}
+            DataType::Boolean => {
+                
self.count_buffer_memory_size(array.as_boolean().values().inner());
+            }
+            DataType::Binary => {
+                self.count_byte_array_memory_size(array.as_binary::<i32>());
+            }
+            DataType::LargeBinary => {
+                self.count_byte_array_memory_size(array.as_binary::<i64>());
+            }
+            DataType::Utf8 => {
+                self.count_byte_array_memory_size(array.as_string::<i32>());
+            }
+            DataType::LargeUtf8 => {
+                self.count_byte_array_memory_size(array.as_string::<i64>());
+            }
+            DataType::BinaryView => {
+                self.count_byte_view_array_memory_size(array.as_binary_view());
+            }
+            DataType::Utf8View => {
+                self.count_byte_view_array_memory_size(array.as_string_view());
+            }
+            DataType::FixedSizeBinary(_) => {
+                
self.count_buffer_memory_size(array.as_fixed_size_binary().values());
+            }
+            DataType::List(_) => {
+                self.count_list_array_memory_size(array.as_list::<i32>());
+            }
+            DataType::LargeList(_) => {
+                self.count_list_array_memory_size(array.as_list::<i64>());
+            }
+            DataType::ListView(_) => {
+                
self.count_list_view_array_memory_size(array.as_list_view::<i32>());
+            }
+            DataType::LargeListView(_) => {
+                
self.count_list_view_array_memory_size(array.as_list_view::<i64>());
+            }
+            DataType::FixedSizeList(_, _) => {
+                self.count_array_memory_size(
+                    array.as_fixed_size_list().values().as_ref(),
+                );
+            }
+            DataType::Struct(_) => {
+                for child in array.as_struct().columns() {
+                    self.count_array_memory_size(child.as_ref());
+                }
+            }
+            DataType::Union(_, _) => {
+                let array = array.as_union();
+                self.count_buffer_memory_size(array.type_ids().inner());
+                if let Some(offsets) = array.offsets() {
+                    self.count_buffer_memory_size(offsets.inner());
+                }
+                for (type_id, _) in array.fields().iter() {
+                    
self.count_array_memory_size(array.child(type_id).as_ref());
+                }
+            }
+            DataType::Dictionary(_, _) => {
+                let array = array.as_any_dictionary();
+                self.count_array_memory_size(array.keys());
+                self.count_array_memory_size(array.values().as_ref());
+            }
+            DataType::Map(_, _) => {
+                let array = array.as_map();
+                self.count_buffer_memory_size(array.offsets().inner().inner());
+                self.count_array_memory_size(array.entries());
+            }
+            DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() 
{
+                DataType::Int16 => {
+                    
self.count_run_array_memory_size::<arrow::datatypes::Int16Type>(
+                        array,
+                    );
+                }
+                DataType::Int32 => {
+                    
self.count_run_array_memory_size::<arrow::datatypes::Int32Type>(
+                        array,
+                    );
+                }
+                DataType::Int64 => {
+                    
self.count_run_array_memory_size::<arrow::datatypes::Int64Type>(
+                        array,
+                    );
+                }
+                // Arrow only permits Int16, Int32, and Int64 run-end indexes. 
A
+                // custom Array implementation may still expose malformed data;
+                // retain correct accounting for it without panicking.
+                _ => self.count_array_data_memory_size(&array.to_data()),
+            },
+            // All currently supported non-primitive layouts are handled above.
+            // The Arrow macro requires a final arm for primitive variants that
+            // its nested dispatch has already consumed. Keep a safe generic
+            // fallback for custom or future Array implementations.
+            _ => self.count_array_data_memory_size(&array.to_data()),
+        }
     }
 
-    // Count all children `ArrayData` recursively
-    for child in array_data.child_data() {
-        count_array_data_memory_size(child, counted_buffers, total_size);
+    fn count_byte_array_memory_size<T: ByteArrayType>(
+        &mut self,
+        array: &GenericByteArray<T>,
+    ) {
+        self.count_buffer_memory_size(array.offsets().inner().inner());
+        self.count_buffer_memory_size(array.values());
+    }
+
+    fn count_byte_view_array_memory_size<T: ByteViewType>(
+        &mut self,
+        array: &GenericByteViewArray<T>,
+    ) {
+        self.count_buffer_memory_size(array.views().inner());
+        for buffer in array.data_buffers() {
+            self.count_buffer_memory_size(buffer);
+        }
+    }
+
+    fn count_list_array_memory_size<O: arrow::array::OffsetSizeTrait>(
+        &mut self,
+        array: &GenericListArray<O>,
+    ) {
+        self.count_buffer_memory_size(array.offsets().inner().inner());
+        self.count_array_memory_size(array.values().as_ref());
+    }
+
+    fn count_list_view_array_memory_size<O: arrow::array::OffsetSizeTrait>(
+        &mut self,
+        array: &GenericListViewArray<O>,
+    ) {
+        self.count_buffer_memory_size(array.offsets().inner());
+        self.count_buffer_memory_size(array.sizes().inner());
+        self.count_array_memory_size(array.values().as_ref());
+    }
+
+    fn count_run_array_memory_size<R: RunEndIndexType>(&mut self, array: &dyn 
Array) {
+        if let Some(array) = array.as_any().downcast_ref::<RunArray<R>>() {
+            self.count_buffer_memory_size(array.run_ends().inner().inner());
+            self.count_array_memory_size(array.values().as_ref());
+        } else {
+            // The DataType and concrete array implementation disagree. Use the
+            // generic representation rather than panic while accounting 
memory.
+            self.count_array_data_memory_size(&array.to_data());
+        }
+    }
+
+    fn count_array_data_memory_size(&mut self, array_data: 
&arrow::array::ArrayData) {
+        for buffer in array_data.buffers() {
+            self.count_buffer_memory_size(buffer);
+        }
+        if let Some(nulls) = array_data.nulls() {
+            self.count_buffer_memory_size(nulls.buffer());
+        }
+        for child in array_data.child_data() {
+            self.count_array_data_memory_size(child);
+        }
+    }
+}
+
+/// Tracks a small number of buffers inline, avoiding a heap allocation for
+/// typical batches, and promotes to a hash set when more buffers are seen.
+#[derive(Debug)]
+struct BufferIdSet {
+    inline: [Option<NonZero<usize>>; INLINE_BUFFER_IDS],
+    len: usize,
+    overflow: Option<HashSet<NonZero<usize>>>,
+}
+
+impl Default for BufferIdSet {
+    fn default() -> Self {
+        Self {
+            inline: [None; INLINE_BUFFER_IDS],
+            len: 0,
+            overflow: None,
+        }
+    }
+}
+
+impl BufferIdSet {
+    fn insert(&mut self, buffer_id: NonZero<usize>) -> bool {
+        if let Some(overflow) = &mut self.overflow {
+            return overflow.insert(buffer_id);
+        }
+
+        if self.inline[..self.len].contains(&Some(buffer_id)) {
+            return false;
+        }
+
+        if self.len < INLINE_BUFFER_IDS {
+            self.inline[self.len] = Some(buffer_id);
+            self.len += 1;
+            return true;
+        }
+
+        let mut overflow = HashSet::with_capacity(INLINE_BUFFER_IDS + 1);
+        overflow.extend(self.inline.iter().flatten().copied());
+        let inserted = overflow.insert(buffer_id);
+        self.overflow = Some(overflow);
+        inserted
     }
 }
 
@@ -247,10 +440,49 @@ mod tests {
 #[cfg(test)]
 mod record_batch_tests {
     use super::*;
-    use arrow::array::{Float64Array, Int32Array, ListArray};
-    use arrow::datatypes::{DataType, Field, Int32Type, Schema};
+    use arrow::array::{
+        ArrayData, ArrayRef, BinaryViewArray, Float64Array, Int16Array, 
Int32Array,
+        Int64Array, LargeListViewArray, ListArray, ListViewArray, RunArray, 
StringArray,
+        StringViewArray, new_null_array,
+    };
+    use arrow::datatypes::{
+        DataType, Field, Int16Type, Int32Type, Int64Type, Schema, UnionFields, 
UnionMode,
+    };
     use std::sync::Arc;
 
+    fn array_data_memory_size(array: &dyn Array) -> usize {
+        fn count(
+            array_data: &ArrayData,
+            counted_buffers: &mut HashSet<NonZero<usize>>,
+            total_size: &mut usize,
+        ) {
+            for buffer in array_data.buffers() {
+                if counted_buffers.insert(buffer.data_ptr().addr()) {
+                    *total_size += buffer.capacity();
+                }
+            }
+            if let Some(nulls) = array_data.nulls() {
+                let buffer = nulls.inner().inner();
+                if counted_buffers.insert(buffer.data_ptr().addr()) {
+                    *total_size += buffer.capacity();
+                }
+            }
+            for child in array_data.child_data() {
+                count(child, counted_buffers, total_size);
+            }
+        }
+
+        let mut total_size = 0;
+        count(&array.to_data(), &mut HashSet::default(), &mut total_size);
+        total_size
+    }
+
+    fn assert_array_memory_size_matches(array: &dyn Array) {
+        let mut counter = RecordBatchMemoryCounter::new();
+        counter.count_array_memory_size(array);
+        assert_eq!(counter.memory_usage(), array_data_memory_size(array));
+    }
+
     #[test]
     fn test_get_record_batch_memory_size() {
         let schema = Arc::new(Schema::new(vec![
@@ -359,6 +591,125 @@ mod record_batch_tests {
         assert_eq!(counter.memory_usage(), 
get_record_batch_memory_size(&batch));
     }
 
+    #[test]
+    fn test_record_batch_memory_counter_promotes_buffer_set() {
+        let fields = (0..=INLINE_BUFFER_IDS)
+            .map(|index| Field::new(format!("col_{index}"), DataType::Int32, 
false))
+            .collect::<Vec<_>>();
+        let columns = (0..=INLINE_BUFFER_IDS)
+            .map(|value| Arc::new(Int32Array::from(vec![value as i32])) as _)
+            .collect::<Vec<_>>();
+        let batch = RecordBatch::try_new(Arc::new(Schema::new(fields)), 
columns).unwrap();
+
+        let mut counter = RecordBatchMemoryCounter::new();
+        assert_eq!(
+            counter.count_batch(&batch),
+            (INLINE_BUFFER_IDS + 1) * size_of::<i32>()
+        );
+        assert!(counter.counted_buffers.overflow.is_some());
+        assert_eq!(counter.count_batch(&batch), 0);
+    }
+
+    #[test]
+    fn test_array_memory_size_matches_array_data_layouts() {
+        let list_field = Arc::new(Field::new_list_field(DataType::Int32, 
true));
+        let struct_fields = vec![Field::new("value", DataType::Int32, 
true)].into();
+        let union_fields = UnionFields::try_new(
+            vec![0],
+            vec![Field::new("value", DataType::Int32, true)],
+        )
+        .unwrap();
+        let map_entries = Arc::new(Field::new(
+            "entries",
+            DataType::Struct(
+                vec![
+                    Field::new("key", DataType::Utf8, false),
+                    Field::new("value", DataType::Int32, true),
+                ]
+                .into(),
+            ),
+            false,
+        ));
+        let data_types = vec![
+            DataType::Boolean,
+            DataType::Int32,
+            DataType::Binary,
+            DataType::LargeBinary,
+            DataType::FixedSizeBinary(4),
+            DataType::BinaryView,
+            DataType::Utf8,
+            DataType::LargeUtf8,
+            DataType::Utf8View,
+            DataType::List(Arc::clone(&list_field)),
+            DataType::LargeList(Arc::clone(&list_field)),
+            DataType::ListView(Arc::clone(&list_field)),
+            DataType::LargeListView(Arc::clone(&list_field)),
+            DataType::FixedSizeList(Arc::clone(&list_field), 2),
+            DataType::Struct(struct_fields),
+            DataType::Union(union_fields, UnionMode::Dense),
+            DataType::Dictionary(Box::new(DataType::Int32), 
Box::new(DataType::Utf8)),
+            DataType::Map(map_entries, false),
+        ];
+
+        for data_type in data_types {
+            let array = new_null_array(&data_type, 3);
+            assert_array_memory_size_matches(array.as_ref());
+        }
+
+        // Exercise the view-specific buffers with concrete, non-empty values.
+        let view_arrays = [
+            Arc::new(BinaryViewArray::from_iter_values([
+                b"short".as_slice(),
+                b"a payload longer than twelve bytes".as_slice(),
+            ])) as ArrayRef,
+            Arc::new(StringViewArray::from_iter_values([
+                "short",
+                "a payload longer than twelve bytes",
+            ])) as ArrayRef,
+            Arc::new(ListViewArray::from_iter_primitive::<Int32Type, _, _>([
+                Some(vec![Some(1), Some(2)]),
+                None,
+                Some(vec![Some(3)]),
+            ])) as ArrayRef,
+            Arc::new(LargeListViewArray::from_iter_primitive::<Int32Type, _, 
_>(
+                [Some(vec![Some(1), Some(2)]), None, Some(vec![Some(3)])],
+            )) as ArrayRef,
+        ];
+
+        for array in view_arrays {
+            assert_array_memory_size_matches(array.as_ref());
+        }
+
+        let run_values = StringArray::from(vec!["alpha", "beta"]);
+        let run_arrays = [
+            Arc::new(
+                RunArray::<Int16Type>::try_new(
+                    &Int16Array::from(vec![2_i16, 5]),
+                    &run_values,
+                )
+                .unwrap(),
+            ) as ArrayRef,
+            Arc::new(
+                RunArray::<Int32Type>::try_new(
+                    &Int32Array::from(vec![2_i32, 5]),
+                    &run_values,
+                )
+                .unwrap(),
+            ) as ArrayRef,
+            Arc::new(
+                RunArray::<Int64Type>::try_new(
+                    &Int64Array::from(vec![2_i64, 5]),
+                    &run_values,
+                )
+                .unwrap(),
+            ) as ArrayRef,
+        ];
+
+        for array in run_arrays {
+            assert_array_memory_size_matches(array.as_ref());
+        }
+    }
+
     #[test]
     fn test_get_record_batch_memory_size_nested_array() {
         let schema = Arc::new(Schema::new(vec![


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to