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/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new a187c7810 perf(table): bound parquet scan split capacity (#2203)
a187c7810 is described below

commit a187c781065980d1a64d996007e9e96223ff69c0
Author: Minh Vu <[email protected]>
AuthorDate: Mon Oct 19 20:11:28 2026 +0200

    perf(table): bound parquet scan split capacity (#2203)
    
    * perf(table): bound parquet scan split capacity
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * test(table): benchmark skewed Parquet scan split sizing
    
    ---------
    
    Signed-off-by: Minh Vu <[email protected]>
---
 table/scan_splits.go               |  10 +-
 table/scan_splits_capacity_test.go | 210 +++++++++++++++++++++++++++++++++++++
 2 files changed, 219 insertions(+), 1 deletion(-)

diff --git a/table/scan_splits.go b/table/scan_splits.go
index 0de390465..a07037e03 100644
--- a/table/scan_splits.go
+++ b/table/scan_splits.go
@@ -51,7 +51,15 @@ func splitParquetScanTask(task FileScanTask, targetSize 
int64) ([]FileScanTask,
                }
        }
 
-       result := make([]FileScanTask, 0, len(offsets))
+       // Each pair of adjacent output tasks covers more than targetSize bytes.
+       // Bound the initial capacity to avoid reserving one task per row group 
when
+       // most row groups will be coalesced. Compare before multiplying to 
avoid
+       // overflow for very large files.
+       capacity := len(offsets)
+       if groups := fileSize / targetSize; groups < int64(capacity/2) {
+               capacity = int(2*groups + 1)
+       }
+       result := make([]FileScanTask, 0, capacity)
        appendTask := func(start, end int64) {
                split := task
                split.Start = start
diff --git a/table/scan_splits_capacity_test.go 
b/table/scan_splits_capacity_test.go
new file mode 100644
index 000000000..497120a2a
--- /dev/null
+++ b/table/scan_splits_capacity_test.go
@@ -0,0 +1,210 @@
+// 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 table
+
+import (
+       "testing"
+
+       "github.com/apache/iceberg-go"
+)
+
+func TestSplitParquetScanTaskCapacity(t *testing.T) {
+       tests := []struct {
+               name           string
+               rowGroups      int
+               rowGroupSize   int64
+               targetSize     int64
+               expectedSplits int
+               maxCapacity    int
+       }{
+               {
+                       name:           "coalesced_small_row_groups",
+                       rowGroups:      4096,
+                       rowGroupSize:   1 << 20,
+                       targetSize:     128 << 20,
+                       expectedSplits: 32,
+                       maxCapacity:    65,
+               },
+               {
+                       name:           "large_row_groups",
+                       rowGroups:      64,
+                       rowGroupSize:   96 << 20,
+                       targetSize:     128 << 20,
+                       expectedSplits: 64,
+                       maxCapacity:    64,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       offsets := make([]int64, tt.rowGroups)
+                       for i := range offsets {
+                               offsets[i] = int64(i) * tt.rowGroupSize
+                       }
+                       fileSize := int64(tt.rowGroups) * tt.rowGroupSize
+                       file := splitTestDataFile(t, iceberg.ParquetFile, 
fileSize, offsets)
+                       task := FileScanTask{File: file, Length: fileSize}
+
+                       result, ok := splitParquetScanTask(task, tt.targetSize)
+                       if !ok || len(result) != tt.expectedSplits {
+                               t.Fatalf("expected %d splits, got %d (ok=%v)", 
tt.expectedSplits, len(result), ok)
+                       }
+                       if cap(result) > tt.maxCapacity {
+                               t.Fatalf("allocated capacity %d, want at most 
%d", cap(result), tt.maxCapacity)
+                       }
+                       for i, split := range result {
+                               if split.Length <= 0 || (i == 0 && split.Start 
!= 0) {
+                                       t.Fatalf("invalid split %d: start=%d 
length=%d", i, split.Start, split.Length)
+                               }
+                               if i > 0 && 
result[i-1].Start+result[i-1].Length != split.Start {
+                                       t.Fatalf("split %d is not contiguous 
with split %d", i-1, i)
+                               }
+                       }
+                       last := result[len(result)-1]
+                       if got := last.Start + last.Length; got != fileSize {
+                               t.Fatalf("split coverage ended at %d, want %d", 
got, fileSize)
+                       }
+               })
+       }
+}
+
+var splitParquetScanTaskBenchmarkSink []FileScanTask
+
+func BenchmarkSplitParquetScanTask(b *testing.B) {
+       tests := []struct {
+               name         string
+               rowGroups    int
+               rowGroupSize int64
+               targetSize   int64
+       }{
+               {"coalesced_4096x1MiB", 4096, 1 << 20, 128 << 20},
+               {"coalesced_1024x1MiB", 1024, 1 << 20, 128 << 20},
+               {"not_coalesced_4096x96MiB", 4096, 96 << 20, 128 << 20},
+       }
+       for _, tt := range tests {
+               b.Run(tt.name, func(b *testing.B) {
+                       offsets := make([]int64, tt.rowGroups)
+                       for i := range offsets {
+                               offsets[i] = int64(i) * tt.rowGroupSize
+                       }
+                       fileSize := int64(tt.rowGroups) * tt.rowGroupSize
+                       builder, err := iceberg.NewDataFileBuilder(
+                               *iceberg.UnpartitionedSpec,
+                               iceberg.EntryContentData,
+                               "mem://table/split-benchmark.parquet",
+                               iceberg.ParquetFile,
+                               nil, nil, nil,
+                               int64(tt.rowGroups),
+                               fileSize,
+                       )
+                       if err != nil {
+                               b.Fatal(err)
+                       }
+                       builder.SplitOffsets(offsets)
+                       task := FileScanTask{File: builder.Build(), Length: 
fileSize}
+
+                       b.ReportAllocs()
+                       b.ResetTimer()
+                       for range b.N {
+                               splits, ok := splitParquetScanTask(task, 
tt.targetSize)
+                               if !ok {
+                                       b.Fatal("expected to split parquet 
task")
+                               }
+                               splitParquetScanTaskBenchmarkSink = splits
+                       }
+               })
+       }
+}
+
+func TestSplitParquetScanTaskSkewedCapacity(t *testing.T) {
+       const rowGroups = 4096
+       const targetSize int64 = 128 << 20
+       offsets := make([]int64, rowGroups)
+       var fileSize int64
+       for i := range offsets {
+               offsets[i] = fileSize
+               if i%2 == 0 {
+                       fileSize += 1 << 20
+               } else {
+                       fileSize += 96 << 20
+               }
+       }
+
+       file := splitTestDataFile(t, iceberg.ParquetFile, fileSize, offsets)
+       splits, ok := splitParquetScanTask(FileScanTask{File: file, Length: 
fileSize}, targetSize)
+       if !ok {
+               t.Fatal("expected skewed Parquet row groups to split")
+       }
+       if len(splits) != 2048 {
+               t.Fatalf("got %d splits, want 2048", len(splits))
+       }
+       if cap(splits) > 3105 {
+               t.Fatalf("allocated capacity %d, want at most 3105", 
cap(splits))
+       }
+
+       var nextStart int64
+       for i, split := range splits {
+               if split.Start != nextStart || split.Length <= 0 {
+                       t.Fatalf("invalid split %d: start=%d length=%d, 
expected start=%d", i, split.Start, split.Length, nextStart)
+               }
+               nextStart += split.Length
+       }
+       if nextStart != fileSize {
+               t.Fatalf("splits cover %d bytes, want %d", nextStart, fileSize)
+       }
+}
+
+func BenchmarkSplitParquetScanTaskSkewed(b *testing.B) {
+       const rowGroups = 4096
+       const targetSize int64 = 128 << 20
+       offsets := make([]int64, rowGroups)
+       var fileSize int64
+       for i := range offsets {
+               offsets[i] = fileSize
+               if i%2 == 0 {
+                       fileSize += 1 << 20
+               } else {
+                       fileSize += 96 << 20
+               }
+       }
+
+       builder, err := iceberg.NewDataFileBuilder(
+               *iceberg.UnpartitionedSpec,
+               iceberg.EntryContentData,
+               "mem://table/skewed-split-benchmark.parquet",
+               iceberg.ParquetFile,
+               nil, nil, nil,
+               rowGroups,
+               fileSize,
+       )
+       if err != nil {
+               b.Fatal(err)
+       }
+       builder.SplitOffsets(offsets)
+       task := FileScanTask{File: builder.Build(), Length: fileSize}
+
+       b.ReportAllocs()
+       b.ResetTimer()
+       for b.Loop() {
+               splits, ok := splitParquetScanTask(task, targetSize)
+               if !ok {
+                       b.Fatal("expected to split Parquet task")
+               }
+               splitParquetScanTaskBenchmarkSink = splits
+       }
+}

Reply via email to