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

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git


The following commit(s) were added to refs/heads/main by this push:
     new bb2390b5 perf(parquet): reuse DataPageV2 assembly buffer for eager 
writes (#1192)
bb2390b5 is described below

commit bb2390b5ad7eadc71236f39579d80514689132af
Author: Minh Vu <[email protected]>
AuthorDate: Mon Aug 31 18:59:46 2026 +0200

    perf(parquet): reuse DataPageV2 assembly buffer for eager writes (#1192)
    
    ### Rationale for this change
    
    `buildDataPageV2` assembles the definition/repetition levels and encoded
    values into a new `bytes.Buffer` for every page. When dictionary
    encoding is disabled or has fallen back, the page is written
    synchronously and the assembly storage can be reused. The current
    allocation pattern adds page-sized churn.
    
    ### What changes are included in this PR?
    
    - Reuse `columnWriter.uncompressedData` for eagerly written DataPageV2
    pages.
    - Keep an owned buffer for pages retained while dictionary encoding is
    active.
    - Add a benchmark for 64 eager 64 KiB pages with uncompressed and Snappy
    codecs.
    - Exercise the eager DataPageV2 path in the existing multi-page
    round-trip test.
    
    Medians from 6 runs on an Apple M1 Pro were:
    
    | codec | metric | main | this PR | change |
    |---|---|---:|---:|---:|
    | uncompressed | time/op | 1.08 ms | 650 us | -39.6% |
    | uncompressed | B/op | 4,547,602 | 383,763 | -91.6% |
    | uncompressed | allocs/op | 461 | 321 | -30.4% |
    | Snappy | time/op | 9.05 ms | 8.88 ms | -1.9% |
    | Snappy | B/op | 3,027,460 | 489,403 | -83.8% |
    | Snappy | allocs/op | 459 | 325 | -29.2% |
    
    ### Are these changes tested?
    
    - `go test ./parquet/... -count=1`
    - `go test ./parquet/file -run
    "^TestBufferedMultiPageDisabledDictionary$" -count=1`
    - `go test ./parquet/file -run "^$" -bench
    "^BenchmarkWriteDataPageV2Eager$" -benchmem -count=6`
    
    The multi-page DataPageV2 test covers the eager path, while the existing
    dictionary coverage keeps the retained-page path covered.
    
    ### Are there any user-facing changes?
    
    No. The Parquet output and public API are unchanged.
---
 parquet/file/column_writer.go           |  25 ++++++--
 parquet/file/file_writer_test.go        | 103 +++++++++++++++++++-------------
 parquet/file/writer_performance_test.go |  54 +++++++++++++++++
 3 files changed, 133 insertions(+), 49 deletions(-)

diff --git a/parquet/file/column_writer.go b/parquet/file/column_writer.go
index 5da1663b..0baaa582 100644
--- a/parquet/file/column_writer.go
+++ b/parquet/file/column_writer.go
@@ -149,6 +149,7 @@ type columnWriter struct {
        defLevelSink *encoding.PooledBufferWriter
        repLevelSink *encoding.PooledBufferWriter
 
+       // Scratch for eagerly assembled pages. It retains the largest page 
until Close.
        uncompressedData bytes.Buffer
        compressedTemp   *bytes.Buffer
 
@@ -398,9 +399,20 @@ func (w *columnWriter) buildDataPageV2(defLevelsRLESize, 
repLevelsRLESize, uncom
        }
 
        // concatenate uncompressed levels and the possibly compressed values
-       var combined bytes.Buffer
-       combined.Grow(int(int64(defLevelsRLESize) + int64(repLevelsRLESize) + 
int64(len(data))))
-       w.concatBuffers(defLevelsRLESize, repLevelsRLESize, data, &combined)
+       bufferedPage := w.hasDict && !w.fallbackToNonDict
+       combinedSize := int(int64(defLevelsRLESize) + int64(repLevelsRLESize) + 
int64(len(data)))
+       var combined []byte
+       if bufferedPage {
+               var owned bytes.Buffer
+               owned.Grow(combinedSize)
+               w.concatBuffers(defLevelsRLESize, repLevelsRLESize, data, 
&owned)
+               combined = owned.Bytes()
+       } else {
+               w.uncompressedData.Reset()
+               w.uncompressedData.Grow(combinedSize)
+               w.concatBuffers(defLevelsRLESize, repLevelsRLESize, data, 
&w.uncompressedData)
+               combined = w.uncompressedData.Bytes()
+       }
 
        pageStats, err := w.getPageStatistics()
        if err != nil {
@@ -417,7 +429,7 @@ func (w *columnWriter) buildDataPageV2(defLevelsRLESize, 
repLevelsRLESize, uncom
        repLevelsByteLen := int32(repLevelsRLESize)
        firstRowIndex := int64(w.rowsWritten)
 
-       page := 
NewDataPageV2WithConfig(memory.NewBufferBytes(combined.Bytes()), nullCount, 
numRows, defLevelsByteLen, repLevelsByteLen,
+       page := NewDataPageV2WithConfig(memory.NewBufferBytes(combined), 
nullCount, numRows, defLevelsByteLen, repLevelsByteLen,
                w.pager.HasCompressor(), DataPageConfig{
                        Num:              numValues,
                        Encoding:         w.encoding,
@@ -425,11 +437,11 @@ func (w *columnWriter) buildDataPageV2(defLevelsRLESize, 
repLevelsRLESize, uncom
                        Stats:            pageStats,
                        FirstRowIndex:    firstRowIndex,
                })
-       if w.hasDict && !w.fallbackToNonDict {
+       if bufferedPage {
                w.totalCompressedBytes += int64(page.buf.Len()) // + sizeof 
pageheader
                w.pages = append(w.pages, page)
        } else {
-               w.totalCompressedBytes += int64(combined.Len())
+               w.totalCompressedBytes += int64(len(combined))
                defer page.Release()
                return w.WriteDataPage(page)
        }
@@ -686,6 +698,7 @@ func (w *columnWriter) resetPageStatistics() {
 func (w *columnWriter) Close() (err error) {
        if !w.closed {
                w.closed = true
+               defer func() { w.uncompressedData = bytes.Buffer{} }()
                if w.hasDict && !w.fallbackToNonDict {
                        if err = w.WriteDictionaryPage(); err != nil {
                                return err
diff --git a/parquet/file/file_writer_test.go b/parquet/file/file_writer_test.go
index 2a28abde..87080779 100644
--- a/parquet/file/file_writer_test.go
+++ b/parquet/file/file_writer_test.go
@@ -310,49 +310,66 @@ func TestBufferedMultiPageDisabledDictionary(t 
*testing.T) {
                valueCount = 10000
                pageSize   = 16384
        )
-       var (
-               sink  = encoding.NewBufferWriter(0, memory.DefaultAllocator)
-               props = 
parquet.NewWriterProperties(parquet.WithDictionaryDefault(false), 
parquet.WithDataPageSize(pageSize))
-               sc, _ = schema.NewGroupNode("schema", 
parquet.Repetitions.Required, schema.FieldList{
-                       schema.NewInt32Node("col", 
parquet.Repetitions.Required, -1),
-               }, -1)
-       )
-
-       writer := file.NewParquetWriter(sink, sc, file.WithWriterProps(props))
-       rgWriter := writer.AppendBufferedRowGroup()
-       cwr, _ := rgWriter.Column(0)
-       cw := cwr.(*file.Int32ColumnChunkWriter)
-       valuesIn := make([]int32, 0, valueCount)
-       for i := int32(0); i < valueCount; i++ {
-               valuesIn = append(valuesIn, (i%100)+1)
-       }
-       cw.WriteBatch(valuesIn, nil, nil)
-       rgWriter.Close()
-       writer.Close()
-       buffer := sink.Finish()
-       defer buffer.Release()
-
-       reader, err := file.NewParquetReader(bytes.NewReader(buffer.Bytes()))
-       assert.NoError(t, err)
-
-       assert.EqualValues(t, 1, reader.NumRowGroups())
-       valuesOut := make([]int32, valueCount)
-
-       for r := 0; r < reader.NumRowGroups(); r++ {
-               rgr := reader.RowGroup(r)
-               assert.EqualValues(t, 1, rgr.NumColumns())
-               assert.EqualValues(t, valueCount, rgr.NumRows())
-
-               var totalRead int64
-               col, err := rgr.Column(0)
-               assert.NoError(t, err)
-               colReader := col.(*file.Int32ColumnChunkReader)
-               for colReader.HasNext() {
-                       total, _, _ := 
colReader.ReadBatch(valueCount-totalRead, valuesOut[totalRead:], nil, nil)
-                       totalRead += total
-               }
-               assert.EqualValues(t, valueCount, totalRead)
-               assert.Equal(t, valuesIn, valuesOut)
+       for _, pageVersion := range []struct {
+               name    string
+               version parquet.DataPageVersion
+       }{
+               {name: "v1", version: parquet.DataPageV1},
+               {name: "v2", version: parquet.DataPageV2},
+       } {
+               t.Run(pageVersion.name, func(t *testing.T) {
+                       sink := encoding.NewBufferWriter(0, 
memory.DefaultAllocator)
+                       props := parquet.NewWriterProperties(
+                               parquet.WithDictionaryDefault(false),
+                               
parquet.WithDataPageVersion(pageVersion.version),
+                               parquet.WithDataPageSize(pageSize),
+                       )
+                       sc, _ := schema.NewGroupNode("schema", 
parquet.Repetitions.Required, schema.FieldList{
+                               schema.NewInt32Node("col", 
parquet.Repetitions.Optional, -1),
+                       }, -1)
+
+                       writer := file.NewParquetWriter(sink, sc, 
file.WithWriterProps(props))
+                       rgWriter := writer.AppendBufferedRowGroup()
+                       cwr, _ := rgWriter.Column(0)
+                       cw := cwr.(*file.Int32ColumnChunkWriter)
+                       valuesIn := make([]int32, 0, valueCount)
+                       defLevels := make([]int16, 0, valueCount)
+                       for i := int32(0); i < valueCount; i++ {
+                               valuesIn = append(valuesIn, (i%100)+1)
+                               defLevels = append(defLevels, 1)
+                       }
+                       _, err := cw.WriteBatch(valuesIn, defLevels, nil)
+                       assert.NoError(t, err)
+                       assert.NoError(t, rgWriter.Close())
+                       assert.NoError(t, writer.Close())
+                       buffer := sink.Finish()
+                       defer buffer.Release()
+
+                       reader, err := 
file.NewParquetReader(bytes.NewReader(buffer.Bytes()))
+                       assert.NoError(t, err)
+                       defer reader.Close()
+
+                       assert.EqualValues(t, 1, reader.NumRowGroups())
+                       valuesOut := make([]int32, valueCount)
+
+                       for r := 0; r < reader.NumRowGroups(); r++ {
+                               rgr := reader.RowGroup(r)
+                               assert.EqualValues(t, 1, rgr.NumColumns())
+                               assert.EqualValues(t, valueCount, rgr.NumRows())
+
+                               var totalRead int64
+                               col, err := rgr.Column(0)
+                               assert.NoError(t, err)
+                               colReader := col.(*file.Int32ColumnChunkReader)
+                               for colReader.HasNext() {
+                                       total, _, err := 
colReader.ReadBatch(valueCount-totalRead, valuesOut[totalRead:], nil, nil)
+                                       assert.NoError(t, err)
+                                       totalRead += total
+                               }
+                               assert.EqualValues(t, valueCount, totalRead)
+                               assert.Equal(t, valuesIn, valuesOut)
+                       }
+               })
        }
 }
 
diff --git a/parquet/file/writer_performance_test.go 
b/parquet/file/writer_performance_test.go
index 0d823769..3f379688 100644
--- a/parquet/file/writer_performance_test.go
+++ b/parquet/file/writer_performance_test.go
@@ -19,10 +19,12 @@ package file_test
 import (
        "bytes"
        "fmt"
+       "io"
        "testing"
 
        "github.com/apache/arrow-go/v18/arrow"
        "github.com/apache/arrow-go/v18/parquet"
+       "github.com/apache/arrow-go/v18/parquet/compress"
        "github.com/apache/arrow-go/v18/parquet/file"
        "github.com/apache/arrow-go/v18/parquet/schema"
 )
@@ -105,6 +107,58 @@ func BenchmarkWriteDictionaryBloomFilter(b *testing.B) {
        }
 }
 
+func BenchmarkWriteDataPageV2Eager(b *testing.B) {
+       sc := schema.NewSchema(schema.MustGroup(schema.NewGroupNode("schema", 
parquet.Repetitions.Required, schema.FieldList{
+               schema.Must(schema.NewPrimitiveNode("data", 
parquet.Repetitions.Required, parquet.Types.Int64, -1, -1)),
+       }, -1)))
+
+       const (
+               pageSize = 64 * 1024
+               numPages = 64
+       )
+       values := make([]int64, pageSize/arrow.Int64SizeBytes*numPages)
+       for i := range values {
+               values[i] = int64(i)
+       }
+
+       for _, codec := range 
[]compress.Compression{compress.Codecs.Uncompressed, compress.Codecs.Snappy} {
+               b.Run(codec.String(), func(b *testing.B) {
+                       props := parquet.NewWriterProperties(
+                               parquet.WithStats(false),
+                               parquet.WithDataPageVersion(parquet.DataPageV2),
+                               parquet.WithDictionaryDefault(false),
+                               parquet.WithCompression(codec),
+                               
parquet.WithBatchSize(int64(pageSize/arrow.Int64SizeBytes)),
+                               parquet.WithDataPageSize(pageSize),
+                       )
+
+                       b.SetBytes(int64(len(values) * arrow.Int64SizeBytes))
+                       b.ReportAllocs()
+                       b.ResetTimer()
+                       for i := 0; i < b.N; i++ {
+                               writer := file.NewParquetWriter(io.Discard, 
sc.Root(), file.WithWriterProps(props))
+                               rgw := writer.AppendRowGroup()
+                               colWriter, err := rgw.NextColumn()
+                               if err != nil {
+                                       b.Fatal(err)
+                               }
+                               if _, err := 
colWriter.(*file.Int64ColumnChunkWriter).WriteBatch(values, nil, nil); err != 
nil {
+                                       b.Fatal(err)
+                               }
+                               if err := colWriter.Close(); err != nil {
+                                       b.Fatal(err)
+                               }
+                               if err := rgw.Close(); err != nil {
+                                       b.Fatal(err)
+                               }
+                               if err := writer.Close(); err != nil {
+                                       b.Fatal(err)
+                               }
+                       }
+               })
+       }
+}
+
 // Benchmark writing small ByteArray values (typical case)
 // This tests the common scenario where values are small (< 1KB)
 func BenchmarkWriteSmallByteArrayValues(b *testing.B) {

Reply via email to