Skip to content

fix: shrink DataPageV2 buffer to fit before converting to Bytes - #10520

Open
vedjaw wants to merge 1 commit into
apache:mainfrom
vedjaw:fix/v2-shrink-to-fit-memory-optimization
Open

fix: shrink DataPageV2 buffer to fit before converting to Bytes#10520
vedjaw wants to merge 1 commit into
apache:mainfrom
vedjaw:fix/v2-shrink-to-fit-memory-optimization

Conversation

@vedjaw

@vedjaw vedjaw commented Aug 2, 2026

Copy link
Copy Markdown

Which issue does this PR close?

Closes #10448.

Rationale for this change

The add_data_page function builds the DataPageV2 buffer by incrementally extending a Vec: first repetition levels, then definition levels, then values (compressed or uncompressed). This incremental growth via extend_from_slice can leave significant excess capacity.

The PARQUET_1_0 (DataPage v1) path already calls shrink_to_fit on its compressed buffer, but the PARQUET_2_0 path never reclaimed the excess capacity before converting the buffer to Bytes and handing it to the page writer.

For dictionary-encoded columns and deferred/buffered page writers, these pages can be retained in memory for extended periods. Each page carrying slack capacity contributes to inflated peak RSS.

What changes are included in this PR?

Adds a buffer.shrink_to_fit() call immediately before the Page::DataPageV2 construction in add_data_page, matching the v1 path's existing behavior.

Are there any user-facing changes?

No API changes. Pages produced by the writer now carry no slack capacity, which may reduce peak memory usage for workloads that use DataPage v2 (the default for parquet::file::properties::WriterProperties when data_page_version is set to V2).

Are these changes tested?

The existing DataPageV2 test suite (100 tests in column::writer::tests) all pass, including roundtrip and compression tests. The change is a memory-management detail and does not alter the serialized page format.

…he#10448)

The DataPageV2 code path in add_data_page builds its buffer by
incrementally extending a Vec - first repetition levels, then
definition levels, then compressed or uncompressed values. This
incremental growth leaves the Vec with excess capacity that is never
reclaimed before the buffer is converted to Bytes and handed to the
page writer.

The PARQUET_1_0 (DataPage v1) path already calls shrink_to_fit on
its compressed buffer, but the PARQUET_2_0 path did not. As a result,
DataPageV2 buffers - especially those held for the lifetime of a
dictionary-encoded column or buffered by a deferred page writer -
retained their over-allocation indefinitely, inflating peak memory
usage for no benefit.

This adds a buffer.shrink_to_fit() call immediately before the
Page::DataPageV2 construction, matching the v1 path's behaviour and
ensuring pages carry no slack capacity into the page writer.
@github-actions github-actions Bot added the parquet Changes to the parquet crate label Aug 2, 2026
@etseidl

etseidl commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

run benchmark arrow_writer

env:
  BENCH_FILTER: parquet_2

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5170556133-1359-2l47q 6.12.85+ #1 SMP Wed Jun 17 20:31:55 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing fix/v2-shrink-to-fit-memory-optimization (53a38ff) to ed92960 (merge-base) diff

Run configuration
run benchmark arrow_writer
env:
  BENCH_FILTER: "parquet_2"

BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench arrow_writer
Results will be posted here when complete


File an issue against this benchmark runner

@etseidl

etseidl commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Thanks @vedjaw, this seems reasonable. Looking over the existing code, I wonder if we could first reserve_exact on the buffers before extending? That might obviate the need to get rid of speculatively allocated memory, at least for uncompressed pages.

Edit: it seems discussion is continuing in the linked issue, so perhaps this is premature.

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing fix/v2-shrink-to-fit-memory-optimization (53a38ff) to ed92960 (merge-base) diff

Run configuration
run benchmark arrow_writer
env:
  BENCH_FILTER: "parquet_2"
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                              fix_v2-shrink-to-fit-memory-optimization    main
-----                                              ----------------------------------------    ----
bool/parquet_2                                     1.02     14.9±0.09ms    16.8 MB/sec         1.00     14.6±0.04ms    17.1 MB/sec
bool/zstd_parquet_2                                1.02     15.3±0.10ms    16.3 MB/sec         1.00     15.0±0.04ms    16.7 MB/sec
bool_non_null/parquet_2                            1.00      8.9±0.04ms    14.1 MB/sec         1.01      9.0±0.03ms    13.9 MB/sec
bool_non_null/zstd_parquet_2                       1.00      9.3±0.05ms    13.4 MB/sec         1.01      9.4±0.03ms    13.3 MB/sec
bool_ree/parquet_2                                 1.02     29.7±0.13ms     7.2 MB/sec         1.00     29.2±0.05ms     7.3 MB/sec
bool_ree/zstd_parquet_2                            1.02     30.1±0.09ms     7.1 MB/sec         1.00     29.6±0.06ms     7.2 MB/sec
decimal/parquet_2                                  1.00    181.5±1.96ms   330.5 MB/sec         1.01    182.6±1.01ms   328.7 MB/sec
decimal/zstd_parquet_2                             1.00    201.3±1.39ms   298.0 MB/sec         1.01    202.8±1.57ms   295.9 MB/sec
fixed_size_binary_ree/parquet_2                    1.01     67.0±0.29ms    15.0 MB/sec         1.00     66.4±0.29ms    15.2 MB/sec
fixed_size_binary_ree/zstd_parquet_2               1.01     67.8±1.03ms    14.9 MB/sec         1.00     67.0±0.24ms    15.0 MB/sec
float_with_nans/parquet_2                          1.00     79.8±0.21ms   175.5 MB/sec         1.01     80.3±0.22ms   174.4 MB/sec
float_with_nans/zstd_parquet_2                     1.00    116.7±0.22ms   120.0 MB/sec         1.00    117.0±0.19ms   119.7 MB/sec
fsb/parquet_2                                      1.00     13.3±0.10ms  1213.6 MB/sec         1.00     13.2±0.07ms  1219.1 MB/sec
fsb/zstd_parquet_2                                 1.00     14.1±0.08ms  1140.3 MB/sec         1.00     14.1±0.07ms  1143.4 MB/sec
int32_ree/parquet_2                                1.00     37.6±0.09ms    10.8 MB/sec         1.01     37.8±0.11ms    10.8 MB/sec
int32_ree/zstd_parquet_2                           1.00     38.1±0.08ms    10.7 MB/sec         1.00     38.2±0.07ms    10.6 MB/sec
int32_ree_95pct_null/parquet_2                     1.00     21.6±0.03ms    18.8 MB/sec         1.00     21.7±0.04ms    18.8 MB/sec
int32_ree_95pct_null/zstd_parquet_2                1.00     21.8±0.02ms    18.7 MB/sec         1.00     21.9±0.03ms    18.6 MB/sec
large_string_non_null/parquet_2                    1.01     54.5±0.08ms     4.6 GB/sec         1.00     54.2±0.11ms     4.6 GB/sec
large_string_non_null/zstd_parquet_2               1.00     54.5±0.09ms     4.6 GB/sec         1.00     54.3±0.10ms     4.6 GB/sec
list_nested/parquet_2                              1.00    145.4±0.20ms   198.4 MB/sec         1.01    146.5±0.17ms   196.9 MB/sec
list_nested/zstd_parquet_2                         1.00    158.2±0.18ms   182.4 MB/sec         1.01    159.4±0.16ms   181.0 MB/sec
list_primitive/parquet_2                           1.00    225.7±0.36ms     2.4 GB/sec         1.01    227.1±0.48ms     2.3 GB/sec
list_primitive/zstd_parquet_2                      1.00    448.1±1.93ms  1216.9 MB/sec         1.00    449.9±1.21ms  1212.2 MB/sec
list_primitive_non_null/parquet_2                  1.09    283.0±5.18ms  1923.0 MB/sec         1.00    260.5±0.45ms     2.0 GB/sec
list_primitive_non_null/zstd_parquet_2             1.00   646.9±10.81ms   841.3 MB/sec         1.00    645.8±0.92ms   842.7 MB/sec
list_primitive_sparse_99pct_null/parquet_2         1.00     12.2±0.03ms     3.0 GB/sec         1.01     12.3±0.04ms     3.0 GB/sec
list_primitive_sparse_99pct_null/zstd_parquet_2    1.00     12.4±0.03ms     3.0 GB/sec         1.01     12.5±0.04ms     2.9 GB/sec
list_struct_with_list/parquet_2                    1.00    275.5±0.47ms   165.5 MB/sec         1.01    276.9±0.90ms   164.7 MB/sec
list_struct_with_list/zstd_parquet_2               1.00    298.8±0.42ms   152.6 MB/sec         1.00    299.5±0.44ms   152.2 MB/sec
primitive/parquet_2                                1.00    136.3±0.56ms   329.2 MB/sec         1.01    137.1±0.37ms   327.4 MB/sec
primitive/zstd_parquet_2                           1.00    169.5±1.12ms   264.8 MB/sec         1.00    170.2±1.17ms   263.7 MB/sec
primitive_all_null/parquet_2                       1.02    284.3±1.54µs   154.1 GB/sec         1.00    279.2±1.42µs   157.0 GB/sec
primitive_all_null/zstd_parquet_2                  1.05    391.8±1.68µs   111.9 GB/sec         1.00    371.8±1.82µs   117.9 GB/sec
primitive_non_null/parquet_2                       1.00     90.1±0.25ms   488.4 MB/sec         1.04     93.3±0.29ms   471.5 MB/sec
primitive_non_null/zstd_parquet_2                  1.00    123.1±0.24ms   357.5 MB/sec         1.03    126.5±0.34ms   347.8 MB/sec
primitive_sparse_99pct_null/parquet_2              1.00     10.8±0.03ms     4.1 GB/sec         1.01     10.9±0.04ms     4.0 GB/sec
primitive_sparse_99pct_null/zstd_parquet_2         1.00     12.7±0.04ms     3.5 GB/sec         1.01     12.8±0.06ms     3.4 GB/sec
short_string_non_null/parquet_2                    1.00     25.6±0.05ms   468.2 MB/sec         1.00     25.7±0.09ms   466.6 MB/sec
short_string_non_null/zstd_parquet_2               1.00     28.6±0.08ms   420.0 MB/sec         1.00     28.6±0.09ms   418.9 MB/sec
string/parquet_2                                   1.00    170.0±0.54ms     3.0 GB/sec         1.09    185.3±2.21ms     2.8 GB/sec
string/zstd_parquet_2                              1.00    385.8±5.22ms  1358.9 MB/sec         1.18    454.5±5.53ms  1153.5 MB/sec
string_and_binary_view/parquet_2                   1.01     60.0±0.33ms   537.2 MB/sec         1.00     59.2±0.13ms   544.6 MB/sec
string_and_binary_view/zstd_parquet_2              1.01     74.0±0.34ms   435.9 MB/sec         1.00     73.2±0.18ms   440.6 MB/sec
string_dictionary/parquet_2                        1.00     49.3±0.36ms     5.2 GB/sec         1.85     91.1±0.30ms     2.8 GB/sec
string_dictionary/zstd_parquet_2                   1.00    192.1±0.20ms  1375.0 MB/sec         1.17    224.9±6.24ms  1174.2 MB/sec
string_non_null/parquet_2                          1.14    124.7±9.31ms     4.1 GB/sec         1.00    109.4±8.07ms     4.7 GB/sec
string_non_null/zstd_parquet_2                     1.00    498.4±5.51ms  1051.4 MB/sec         1.00    496.4±5.94ms  1055.6 MB/sec
string_ree/parquet_2                               1.00    170.4±0.39ms    96.3 MB/sec         1.02    173.6±0.81ms    94.5 MB/sec
string_ree/zstd_parquet_2                          1.00    186.0±0.30ms    88.2 MB/sec         1.01    188.5±0.72ms    87.1 MB/sec
string_ree_95pct_null/parquet_2                    1.00     24.8±0.09ms    56.8 MB/sec         1.01     24.9±0.07ms    56.5 MB/sec
string_ree_95pct_null/zstd_parquet_2               1.00     25.5±0.10ms    55.1 MB/sec         1.01     25.7±0.09ms    54.6 MB/sec
struct_all_null/parquet_2                          1.03    121.6±0.71µs   129.5 GB/sec         1.00    118.6±0.79µs   132.8 GB/sec
struct_all_null/zstd_parquet_2                     1.06    169.5±0.78µs    92.9 GB/sec         1.00    160.0±0.74µs    98.4 GB/sec
struct_non_null/parquet_2                          1.00     41.3±0.08ms   387.2 MB/sec         1.01     41.6±0.12ms   384.9 MB/sec
struct_non_null/zstd_parquet_2                     1.00     55.1±0.12ms   290.5 MB/sec         1.00     55.2±0.10ms   289.8 MB/sec
struct_sparse_99pct_null/parquet_2                 1.00      6.3±0.04ms     2.5 GB/sec         1.00      6.3±0.02ms     2.5 GB/sec
struct_sparse_99pct_null/zstd_parquet_2            1.00      7.1±0.01ms     2.2 GB/sec         1.00      7.1±0.02ms     2.2 GB/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 1025.2s
Peak memory 2.9 GiB
Avg memory 2.6 GiB
CPU user 973.6s
CPU sys 49.4s
Peak spill 0 B

branch

Metric Value
Wall time 1020.2s
Peak memory 2.9 GiB
Avg memory 2.6 GiB
CPU user 982.2s
CPU sys 34.1s
Peak spill 0 B

File an issue against this benchmark runner

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

parquet Changes to the parquet crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parquet: Should DataPageV2 shrink_to_fit?

3 participants