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 35aa462d perf(parquet/pqarrow): avoid goroutines for serial struct 
reads (#1250)
35aa462d is described below

commit 35aa462d065210881a9d7b96ae385f1961aa2057
Author: Minh Vu <[email protected]>
AuthorDate: Mon Aug 31 17:42:15 2026 +0200

    perf(parquet/pqarrow): avoid goroutines for serial struct reads (#1250)
    
    ## Summary
    
    - Use direct child loops in `structReader.SeekToRow` and `LoadBatch`
    when `Parallel=false`.
    - Keep the existing `errgroup` path for parallel reads.
    - Continue visiting every child after an error and return the first
    error, matching the old serial behavior.
    - Add a regression test and a nested struct benchmark.
    
    ## Benchmark
    
    Apple M1 Pro. `upstream/main` at `6b039a76`. 500ms per sample, 5
    samples.
    
    The benchmark reads a 1024-row nested struct across 16 uncompressed row
    groups and includes `SeekToRow(0)` plus `NextBatch(1024)`.
    
    | children | main | this PR | change | allocs/op |
    | ---: | ---: | ---: | ---: | ---: |
    | 1 | 120.5 us | 103.6 us | 14.1% faster | 723 -> 715 |
    | 8 | 869.1 us | 784.0 us | 9.8% faster | 5708 -> 5672 |
    | 32 | 2.927 ms | 2.614 ms | 10.7% faster | 23300 -> 23166 |
    | 128 | 10.017 ms | 9.140 ms | 8.8% faster | 93066 -> 92549 |
    
    Command:
    
    ```text
    go test ./parquet/pqarrow -run '^$' -bench 
'^BenchmarkReadNestedStructSerial$' -benchmem -benchtime=500ms -count=5
    ```
    
    ## Checks
    
    - `PARQUET_TEST_DATA=<parquet-testing-data> go test ./parquet/...`
    - `go test -race ./parquet/pqarrow -run
    
'^(TestStructReaderSerialOperationsVisitEveryChild|TestRecordReaderSerial|TestRecordReaderParallel|TestRecordReaderSeekToRow|TestPartialStructColumnRead)$'
    -count=1`
    - `go vet ./parquet/pqarrow`
    - `git diff --check`
---
 parquet/pqarrow/column_readers.go           |  24 +++--
 parquet/pqarrow/struct_reader_bench_test.go | 131 ++++++++++++++++++++++++++++
 parquet/pqarrow/struct_reader_test.go       | 120 +++++++++++++++++++++++++
 3 files changed, 270 insertions(+), 5 deletions(-)

diff --git a/parquet/pqarrow/column_readers.go 
b/parquet/pqarrow/column_readers.go
index 3456fccb..b90072ea 100644
--- a/parquet/pqarrow/column_readers.go
+++ b/parquet/pqarrow/column_readers.go
@@ -295,12 +295,19 @@ func (sr *structReader) GetRepLevels() ([]int16, error) {
 }
 
 func (sr *structReader) SeekToRow(rowIdx int64) error {
-       var g errgroup.Group
        if !sr.props.Parallel {
-               g.SetLimit(1)
+               var firstErr error
+               for _, rdr := range sr.children {
+                       if err := rdr.SeekToRow(rowIdx); err != nil && firstErr 
== nil {
+                               firstErr = err
+                       }
+               }
+               return firstErr
        }
 
+       var g errgroup.Group
        for _, rdr := range sr.children {
+               rdr := rdr
                g.Go(func() error {
                        return rdr.SeekToRow(rowIdx)
                })
@@ -310,14 +317,21 @@ func (sr *structReader) SeekToRow(rowIdx int64) error {
 }
 
 func (sr *structReader) LoadBatch(nrecords int64) error {
+       if !sr.props.Parallel {
+               var firstErr error
+               for _, rdr := range sr.children {
+                       if err := rdr.LoadBatch(nrecords); err != nil && 
firstErr == nil {
+                               firstErr = err
+                       }
+               }
+               return firstErr
+       }
+
        // Load batches in parallel
        // When reading structs with large numbers of columns, the serial load 
is very slow.
        // This is especially true when reading Cloud Storage. Loading 
concurrently
        // greatly improves performance.
        g := new(errgroup.Group)
-       if !sr.props.Parallel {
-               g.SetLimit(1)
-       }
        for _, rdr := range sr.children {
                rdr := rdr
                g.Go(func() error {
diff --git a/parquet/pqarrow/struct_reader_bench_test.go 
b/parquet/pqarrow/struct_reader_bench_test.go
new file mode 100644
index 00000000..682028a7
--- /dev/null
+++ b/parquet/pqarrow/struct_reader_bench_test.go
@@ -0,0 +1,131 @@
+// 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 pqarrow_test
+
+import (
+       "bytes"
+       "context"
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "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/pqarrow"
+)
+
+func BenchmarkReadNestedStructSerial(b *testing.B) {
+       const (
+               nrows        = 1024
+               rowGroupSize = 64
+       )
+
+       for _, nchildren := range []int{1, 8, 32, 128} {
+               b.Run(fmt.Sprintf("children=%d", nchildren), func(b *testing.B) 
{
+                       mem := memory.DefaultAllocator
+                       tbl := makeWideNestedInt32Table(mem, nchildren, nrows)
+                       defer tbl.Release()
+
+                       var buf bytes.Buffer
+                       writerProps := 
parquet.NewWriterProperties(parquet.WithCompression(compress.Codecs.Uncompressed))
+                       if err := pqarrow.WriteTable(tbl, &buf, rowGroupSize, 
writerProps, pqarrow.DefaultWriterProps()); err != nil {
+                               b.Fatal(err)
+                       }
+                       parquetData := buf.Bytes()
+
+                       pf, err := 
file.NewParquetReader(bytes.NewReader(parquetData))
+                       if err != nil {
+                               b.Fatal(err)
+                       }
+                       defer pf.Close()
+
+                       reader, err := pqarrow.NewFileReader(pf, 
pqarrow.ArrowReadProperties{
+                               BatchSize: nrows,
+                               Parallel:  false,
+                       }, mem)
+                       if err != nil {
+                               b.Fatal(err)
+                       }
+
+                       rowGroups := make([]int, nrows/rowGroupSize)
+                       for i := range rowGroups {
+                               rowGroups[i] = i
+                       }
+                       includedLeaves := make(map[int]bool, nchildren)
+                       for i := 0; i < nchildren; i++ {
+                               includedLeaves[i] = true
+                       }
+                       fieldReader, err := 
reader.GetFieldReader(context.Background(), 0, includedLeaves, rowGroups)
+                       if err != nil {
+                               b.Fatal(err)
+                       }
+                       defer fieldReader.Release()
+
+                       b.ReportAllocs()
+                       b.SetBytes(int64(len(parquetData)))
+                       b.ResetTimer()
+                       for range b.N {
+                               if err := fieldReader.SeekToRow(0); err != nil {
+                                       b.Fatal(err)
+                               }
+                               out, err := fieldReader.NextBatch(nrows)
+                               if err != nil {
+                                       b.Fatal(err)
+                               }
+                               out.Release()
+                       }
+               })
+       }
+}
+
+func makeWideNestedInt32Table(mem memory.Allocator, nchildren, nrows int) 
arrow.Table {
+       childFields := make([]arrow.Field, nchildren)
+       for i := range childFields {
+               childFields[i] = arrow.Field{
+                       Name: fmt.Sprintf("child_%d", i),
+                       Type: arrow.PrimitiveTypes.Int32,
+               }
+       }
+
+       structType := arrow.StructOf(childFields...)
+       schema := arrow.NewSchema([]arrow.Field{{Name: "nested", Type: 
structType}}, nil)
+       builder := array.NewStructBuilder(mem, structType)
+       defer builder.Release()
+
+       valid := make([]bool, nrows)
+       values := make([]int32, nrows)
+       for i := range valid {
+               valid[i] = true
+               values[i] = int32(i)
+       }
+       builder.AppendValues(valid)
+       for i := 0; i < nchildren; i++ {
+               
builder.FieldBuilder(i).(*array.Int32Builder).AppendValues(values, nil)
+       }
+
+       arr := builder.NewStructArray()
+       chunked := arrow.NewChunked(structType, []arrow.Array{arr})
+       column := arrow.NewColumn(schema.Field(0), chunked)
+       table := array.NewTable(schema, []arrow.Column{*column}, int64(nrows))
+       column.Release()
+       chunked.Release()
+       arr.Release()
+       return table
+}
diff --git a/parquet/pqarrow/struct_reader_test.go 
b/parquet/pqarrow/struct_reader_test.go
new file mode 100644
index 00000000..65762191
--- /dev/null
+++ b/parquet/pqarrow/struct_reader_test.go
@@ -0,0 +1,120 @@
+// 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 pqarrow
+
+import (
+       "errors"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/stretchr/testify/require"
+)
+
+type structReaderTestChild struct {
+       name      string
+       events    *[]string
+       seekRow   int64
+       batchSize int64
+       seekErr   error
+       loadErr   error
+}
+
+func (r *structReaderTestChild) LoadBatch(nrecords int64) error {
+       *r.events = append(*r.events, r.name)
+       r.batchSize = nrecords
+       return r.loadErr
+}
+
+func (r *structReaderTestChild) BuildArray(int64) (*arrow.Chunked, error) { 
return nil, nil }
+
+func (r *structReaderTestChild) GetDefLevels() ([]int16, error) { return nil, 
nil }
+
+func (r *structReaderTestChild) GetRepLevels() ([]int16, error) { return nil, 
nil }
+
+func (r *structReaderTestChild) Field() *arrow.Field {
+       return &arrow.Field{Name: r.name, Type: arrow.PrimitiveTypes.Int32}
+}
+
+func (r *structReaderTestChild) SeekToRow(row int64) error {
+       *r.events = append(*r.events, r.name)
+       r.seekRow = row
+       return r.seekErr
+}
+
+func (r *structReaderTestChild) IsOrHasRepeatedChild() bool { return false }
+
+func (r *structReaderTestChild) Retain() {}
+
+func (r *structReaderTestChild) Release() {}
+
+func TestStructReaderSerialOperationsVisitEveryChild(t *testing.T) {
+       seekErr := errors.New("seek failed")
+       loadErr := errors.New("load failed")
+
+       tests := []struct {
+               name        string
+               call        func(*structReader) error
+               expectedErr error
+               check       func(*testing.T, []*structReaderTestChild)
+       }{
+               {
+                       name:        "seek to row",
+                       call:        func(reader *structReader) error { return 
reader.SeekToRow(42) },
+                       expectedErr: seekErr,
+                       check: func(t *testing.T, children 
[]*structReaderTestChild) {
+                               for _, child := range children {
+                                       require.Equal(t, int64(42), 
child.seekRow)
+                               }
+                       },
+               },
+               {
+                       name:        "load batch",
+                       call:        func(reader *structReader) error { return 
reader.LoadBatch(128) },
+                       expectedErr: loadErr,
+                       check: func(t *testing.T, children 
[]*structReaderTestChild) {
+                               for _, child := range children {
+                                       require.Equal(t, int64(128), 
child.batchSize)
+                               }
+                       },
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       events := make([]string, 0, 3)
+                       children := []*structReaderTestChild{
+                               {name: "first", events: &events},
+                               {name: "second", events: &events, seekErr: 
seekErr, loadErr: loadErr},
+                               {name: "third", events: &events, seekErr: 
errors.New("later seek failed"), loadErr: errors.New("later load failed")},
+                       }
+
+                       readers := make([]*ColumnReader, len(children))
+                       for i, child := range children {
+                               readers[i] = &ColumnReader{colReaderImpl: child}
+                       }
+
+                       reader := &structReader{children: readers}
+                       err := tt.call(reader)
+
+                       require.ErrorIs(t, err, tt.expectedErr, "the first 
child error should be returned")
+                       require.Equal(t, []string{"first", "second", "third"}, 
events)
+                       tt.check(t, children)
+               })
+       }
+}
+
+var _ colReaderImpl = (*structReaderTestChild)(nil)

Reply via email to