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 08c90684 perf(parquet/compress): reuse Brotli writers across encodes 
(#1254)
08c90684 is described below

commit 08c906849b23edb15e05b5840608e96c69b3bff1
Author: Minh Vu <[email protected]>
AuthorDate: Wed Sep 2 19:55:20 2026 +0200

    perf(parquet/compress): reuse Brotli writers across encodes (#1254)
    
    ## Summary
    
    - **Reuse Brotli writers** for one-shot `Encode` and `EncodeLevel`
    calls.
    - Keep a separate `sync.Pool` for each supported compression level.
    - Reset pooled writers with a nil destination before putting them back.
    - Keep the streaming writer APIs unchanged.
    - Add serial and parallel benchmarks plus sequential and concurrent
    correctness tests.
    
    ## Benchmark
    
    Compared with `main` at `6b039a76`:
    
    | Case | Main | This PR | Main allocations | This PR allocations |
    | --- | ---: | ---: | ---: | ---: |
    | compressible / 256 KiB | ~0.78 ms/op | ~0.51 ms/op | ~22 allocs/op,
    ~11.9 MB/op | 1 alloc/op, ~10 KB/op |
    | semi-random / 256 KiB | ~2.04 ms/op | ~1.77 ms/op | ~21 allocs/op,
    ~12.0 MB/op | 1 alloc/op, ~37 KB/op |
    
    The parallel benchmark also stays at one allocation per operation in the
    pooled path.
    
    ## Checks
    
    - `go test ./parquet/compress -count=1`
    - `go test -race ./parquet/compress`
    - `go vet ./parquet/compress`
    - Parquet package tests with `PARQUET_TEST_DATA` set
---
 parquet/compress/brotli.go                |  55 +++++++++++++++-
 parquet/compress/brotli_benchmark_test.go | 105 ++++++++++++++++++++++++++++++
 parquet/compress/brotli_internal_test.go  |  63 ++++++++++++++++++
 parquet/compress/compress_test.go         |  60 +++++++++++++++++
 4 files changed, 281 insertions(+), 2 deletions(-)

diff --git a/parquet/compress/brotli.go b/parquet/compress/brotli.go
index fdc0346e..ca058ed1 100644
--- a/parquet/compress/brotli.go
+++ b/parquet/compress/brotli.go
@@ -27,6 +27,24 @@ import (
 
 type brotliCodec struct{}
 
+const (
+       // Brotli qualities above the default retain substantially larger 
encoder
+       // workspaces. Keep those writers transient so a high-quality request 
does
+       // not permanently increase the process memory footprint.
+       maxPooledBrotliLevel = brotli.DefaultCompression
+       brotliWriterPoolSize = 1
+)
+
+var brotliWriterPools = newBrotliWriterPools()
+
+func newBrotliWriterPools() [brotli.BestCompression + 1]chan *brotli.Writer {
+       var pools [brotli.BestCompression + 1]chan *brotli.Writer
+       for level := brotli.BestSpeed; level <= maxPooledBrotliLevel; level++ {
+               pools[level] = make(chan *brotli.Writer, brotliWriterPoolSize)
+       }
+       return pools
+}
+
 func (brotliCodec) NewReader(r io.Reader) io.ReadCloser {
        return io.NopCloser(brotli.NewReader(r))
 }
@@ -41,15 +59,48 @@ func (b brotliCodec) EncodeLevel(dst, src []byte, level 
int) []byte {
                dst = make([]byte, 0, maxlen)
        }
        buf := bytes.NewBuffer(dst[:0])
-       w := brotli.NewWriterLevel(buf, level)
+       pool := brotliWriterPool(level)
+       var w *brotli.Writer
+       if pool != nil {
+               select {
+               case w = <-pool:
+                       w.Reset(buf)
+               default:
+               }
+       }
+       if w == nil {
+               w = brotli.NewWriterLevel(buf, level)
+       }
        _, err := w.Write(src)
        if err != nil {
+               releaseBrotliWriter(pool, w)
                panic(err)
        }
        if err := w.Close(); err != nil {
+               releaseBrotliWriter(pool, w)
                panic(err)
        }
-       return buf.Bytes()
+       compressed := buf.Bytes()
+       releaseBrotliWriter(pool, w)
+       return compressed
+}
+
+func brotliWriterPool(level int) chan *brotli.Writer {
+       if level < brotli.BestSpeed || level > maxPooledBrotliLevel {
+               return nil
+       }
+       return brotliWriterPools[level]
+}
+
+func releaseBrotliWriter(pool chan *brotli.Writer, w *brotli.Writer) {
+       if pool == nil {
+               return
+       }
+       w.Reset(nil)
+       select {
+       case pool <- w:
+       default:
+       }
 }
 
 func (b brotliCodec) Encode(dst, src []byte) []byte {
diff --git a/parquet/compress/brotli_benchmark_test.go 
b/parquet/compress/brotli_benchmark_test.go
new file mode 100644
index 00000000..c1071550
--- /dev/null
+++ b/parquet/compress/brotli_benchmark_test.go
@@ -0,0 +1,105 @@
+// 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.
+
+package compress_test
+
+import (
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/parquet/compress"
+)
+
+func BenchmarkBrotliEncodeLevel(b *testing.B) {
+       dataCases := []struct {
+               name string
+               data []byte
+       }{
+               {name: "compressible/64KiB", data: makeCompressibleData(64 * 
1024)},
+               {name: "compressible/256KiB", data: makeCompressibleData(256 * 
1024)},
+               {name: "compressible/1MiB", data: makeCompressibleData(1024 * 
1024)},
+               {name: "semi-random/64KiB", data: makeSemiRandomBrotliData(64 * 
1024)},
+               {name: "semi-random/256KiB", data: makeSemiRandomBrotliData(256 
* 1024)},
+               {name: "semi-random/1MiB", data: makeSemiRandomBrotliData(1024 
* 1024)},
+       }
+       levels := []struct {
+               name  string
+               level int
+       }{
+               {name: "level=1", level: 1},
+               {name: "level=default", level: 
compress.DefaultCompressionLevel},
+               {name: "level=9", level: 9},
+               {name: "level=11", level: 11},
+       }
+
+       codec, err := compress.GetCodec(compress.Codecs.Brotli)
+       if err != nil {
+               b.Fatal(err)
+       }
+
+       for _, dataCase := range dataCases {
+               for _, level := range levels {
+                       b.Run(fmt.Sprintf("%s/%s", dataCase.name, level.name), 
func(b *testing.B) {
+                               dst := make([]byte, 
int(codec.CompressBound(int64(len(dataCase.data)))))
+                               b.SetBytes(int64(len(dataCase.data)))
+                               b.ReportAllocs()
+                               b.ResetTimer()
+                               for range b.N {
+                                       codec.EncodeLevel(dst, dataCase.data, 
level.level)
+                               }
+                       })
+               }
+       }
+}
+
+func BenchmarkBrotliEncodeLevelParallel(b *testing.B) {
+       data := makeSemiRandomBrotliData(256 * 1024)
+       codec, err := compress.GetCodec(compress.Codecs.Brotli)
+       if err != nil {
+               b.Fatal(err)
+       }
+
+       for _, level := range []struct {
+               name  string
+               level int
+       }{
+               {name: "level=1", level: 1},
+               {name: "level=default", level: 
compress.DefaultCompressionLevel},
+               {name: "level=9", level: 9},
+               {name: "level=11", level: 11},
+       } {
+               b.Run(level.name, func(b *testing.B) {
+                       b.SetBytes(int64(len(data)))
+                       b.ReportAllocs()
+                       b.ResetTimer()
+                       b.RunParallel(func(pb *testing.PB) {
+                               dst := make([]byte, 
int(codec.CompressBound(int64(len(data)))))
+                               for pb.Next() {
+                                       codec.EncodeLevel(dst, data, 
level.level)
+                               }
+                       })
+               })
+       }
+}
+
+func makeSemiRandomBrotliData(size int) []byte {
+       data := makeRandomData(size)
+       pattern := []byte("parquet-page-data-pattern-0123456789abcdef")
+       for i := 0; i < len(data)/4; i += len(pattern) {
+               copy(data[i:], pattern)
+       }
+       return data
+}
diff --git a/parquet/compress/brotli_internal_test.go 
b/parquet/compress/brotli_internal_test.go
new file mode 100644
index 00000000..29946c08
--- /dev/null
+++ b/parquet/compress/brotli_internal_test.go
@@ -0,0 +1,63 @@
+// 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.
+
+package compress
+
+import (
+       "bytes"
+       "testing"
+
+       "github.com/andybalholm/brotli"
+       "github.com/stretchr/testify/assert"
+)
+
+func TestBrotliWriterPoolMemoryPolicy(t *testing.T) {
+       for level := brotli.BestSpeed; level <= brotli.BestCompression; level++ 
{
+               pool := brotliWriterPool(level)
+               if level <= maxPooledBrotliLevel {
+                       assert.NotNil(t, pool, "level %d should be pooled", 
level)
+                       assert.Equal(t, brotliWriterPoolSize, cap(pool), "level 
%d pool size", level)
+               } else {
+                       assert.Nil(t, pool, "level %d should not retain its 
workspace", level)
+               }
+       }
+
+       for _, level := range []int{brotli.BestSpeed - 1, 
brotli.BestCompression + 1} {
+               assert.Nil(t, brotliWriterPool(level), "invalid level %d should 
not be pooled", level)
+       }
+}
+
+func TestBrotliHighQualityWritersAreNotRetained(t *testing.T) {
+       const pattern = "parquet-page-data-pattern-0123456789abcdef"
+       src := bytes.Repeat([]byte(pattern), 
(256*1024+len(pattern)-1)/len(pattern))
+       codec := brotliCodec{}
+
+       for level := maxPooledBrotliLevel + 1; level <= brotli.BestCompression; 
level++ {
+               codec.EncodeLevel(nil, src, level)
+               assert.Nil(t, brotliWriterPool(level), "level %d should not 
retain its workspace", level)
+       }
+}
+
+func TestReleaseBrotliWriterIsBounded(t *testing.T) {
+       pool := make(chan *brotli.Writer, brotliWriterPoolSize)
+       w1 := brotli.NewWriterLevel(nil, brotli.DefaultCompression)
+       w2 := brotli.NewWriterLevel(nil, brotli.DefaultCompression)
+
+       releaseBrotliWriter(pool, w1)
+       releaseBrotliWriter(pool, w2)
+
+       assert.Len(t, pool, brotliWriterPoolSize)
+}
diff --git a/parquet/compress/compress_test.go 
b/parquet/compress/compress_test.go
index 9f6e38d3..5fe8f867 100644
--- a/parquet/compress/compress_test.go
+++ b/parquet/compress/compress_test.go
@@ -259,6 +259,66 @@ func TestBrotliCompressBound(t *testing.T) {
        }
 }
 
+func TestBrotliEncodeLevelReuse(t *testing.T) {
+       codec, err := compress.GetCodec(compress.Codecs.Brotli)
+       assert.NoError(t, err)
+
+       tests := []struct {
+               name string
+               src  []byte
+       }{
+               {name: "empty", src: nil},
+               {name: "one byte", src: []byte{0xab}},
+               {name: "compressible", src: makeCompressibleData(4 * 1024)},
+               {name: "random", src: makeRandomData(4 * 1024)},
+       }
+       levels := []int{0, compress.DefaultCompressionLevel, 5, 11, -2, 12}
+
+       for _, tt := range tests {
+               dst := make([]byte, 0, 
int(codec.CompressBound(int64(len(tt.src)))))
+               for _, level := range levels {
+                       compressed := codec.EncodeLevel(dst, tt.src, level)
+                       decoded, err := compress.Decode(codec, nil, compressed)
+                       if !assert.NoError(t, err, "%s, level %d", tt.name, 
level) {
+                               continue
+                       }
+                       assert.True(t, bytes.Equal(tt.src, decoded), "%s, level 
%d", tt.name, level)
+                       dst = compressed[:0]
+               }
+       }
+}
+
+func TestBrotliEncodeLevelConcurrent(t *testing.T) {
+       codec, err := compress.GetCodec(compress.Codecs.Brotli)
+       assert.NoError(t, err)
+
+       src := makeCompressibleData(4 * 1024)
+       levels := []int{0, compress.DefaultCompressionLevel, 9, 11}
+
+       const workers = 16
+       var wg sync.WaitGroup
+       wg.Add(workers)
+       for i := 0; i < workers; i++ {
+               go func(i int) {
+                       defer wg.Done()
+                       for j := 0; j < len(levels); j++ {
+                               level := levels[(i+j)%len(levels)]
+                               compressed := codec.EncodeLevel(nil, src, level)
+                               decoded, err := compress.Decode(codec, nil, 
compressed)
+                               if err != nil {
+                                       t.Errorf("level %d: decode failed: %v", 
level, err)
+                                       return
+                               }
+                               if !bytes.Equal(src, decoded) {
+                                       t.Errorf("level %d: decoded data 
differs", level)
+                                       return
+                               }
+                       }
+               }(i)
+       }
+       wg.Wait()
+}
+
 func TestCompressReaderWriter(t *testing.T) {
        tests := []struct {
                c compress.Compression

Reply via email to