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 f49e311e2 perf(table): append local Parquet splits without per-file 
buffers (#2269)
f49e311e2 is described below

commit f49e311e21a5fdb0069477f1bc7fb1e5e3a203f2
Author: Minh Vu <[email protected]>
AuthorDate: Fri Oct 23 18:28:51 2026 +0200

    perf(table): append local Parquet splits without per-file buffers (#2269)
    
    * perf(table): append local Parquet splits without per-file buffers
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * test(table): cover mixed local scan splits
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * style(table): gofmt split benchmark constants
    
    ---------
    
    Signed-off-by: Minh Vu <[email protected]>
---
 table/scan_task_finalization.go                  | 11 +--
 table/scan_task_finalization_split_bench_test.go | 93 ++++++++++++++++++++++++
 table/scan_task_finalization_split_test.go       | 65 +++++++++++++++++
 3 files changed, 164 insertions(+), 5 deletions(-)

diff --git a/table/scan_task_finalization.go b/table/scan_task_finalization.go
index 1e51b55f6..723a0d584 100644
--- a/table/scan_task_finalization.go
+++ b/table/scan_task_finalization.go
@@ -184,15 +184,16 @@ func (scan *Scan) finalizeTaskRange(
 
                result.metrics.addResultDeleteMetrics(*task)
                result.metrics.totalFileSize += task.File.FileSizeBytes()
-               if splitTasks, split := splitParquetScanTask(*task, 
splitTargetSize); split {
+               if offsets, fileSize, split := splittableParquetOffsets(*task, 
splitTargetSize); split {
                        if !result.expanded {
                                result.expanded = true
-                               // A mixed range keeps the unsplit prefix; 
reserve extra space
-                               // once to avoid repeated append growth when 
more files split.
-                               result.tasks = make([]FileScanTask, 0, 
max((end-start)*2, index-start+len(splitTasks)))
+                               // A mixed range keeps the unsplit prefix. 
Reserve space once
+                               // without allocating a temporary slice for 
each split file.
+                               result.tasks = make([]FileScanTask, 0, 
max((end-start)*2,
+                                       
index-start+parquetSplitCapacity(len(offsets), fileSize, splitTargetSize)))
                                result.tasks = append(result.tasks, 
plannedTasks[start:index]...)
                        }
-                       result.tasks = append(result.tasks, splitTasks...)
+                       result.tasks = appendParquetTaskSplits(result.tasks, 
*task, offsets, fileSize, splitTargetSize)
                } else if result.expanded {
                        result.tasks = append(result.tasks, *task)
                }
diff --git a/table/scan_task_finalization_split_bench_test.go 
b/table/scan_task_finalization_split_bench_test.go
new file mode 100644
index 000000000..07f2a2c5f
--- /dev/null
+++ b/table/scan_task_finalization_split_bench_test.go
@@ -0,0 +1,93 @@
+// 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 (
+       "fmt"
+       "testing"
+
+       "github.com/apache/iceberg-go"
+)
+
+var finalizeTaskRangeSplitBenchmarkSink []FileScanTask
+
+// BenchmarkFinalizeTaskRangeParquetSplits isolates local scan task 
finalization.
+// Files and split offsets are built before timing. No residual evaluation or
+// manifest I/O is included, so the result measures split/append overhead.
+func BenchmarkFinalizeTaskRangeParquetSplits(b *testing.B) {
+       const (
+               rowGroupBytes int64 = 4096
+               targetSize    int64 = 16384
+       )
+
+       for _, tc := range []struct {
+               files, groups, splittablePercent int
+       }{
+               {8192, 32, 0},
+               {8192, 32, 50},
+               {8192, 32, 100},
+               {2048, 128, 100},
+       } {
+               b.Run(fmt.Sprintf("files=%d/groups=%d/splittable=%d",
+                       tc.files, tc.groups, tc.splittablePercent), func(b 
*testing.B) {
+                       offsets := make([]int64, tc.groups)
+                       for i := range offsets {
+                               offsets[i] = int64(i) * rowGroupBytes
+                       }
+                       fileSize := int64(tc.groups) * rowGroupBytes
+                       tasks := make([]FileScanTask, tc.files)
+                       for i := range tasks {
+                               file := finalizationTestDataFile(
+                                       b, *iceberg.UnpartitionedSpec,
+                                       
fmt.Sprintf("mem://finalization-splits/data-%d.parquet", i),
+                                       nil, fileSize, offsets,
+                               )
+                               task := FileScanTask{File: file, Length: 
fileSize}
+                               if i%100 >= tc.splittablePercent {
+                                       task.Start = 1
+                                       task.Length--
+                               }
+                               tasks[i] = task
+                       }
+
+                       expectedTasks := 0
+                       for _, task := range tasks {
+                               if splits, ok := splitParquetScanTask(task, 
targetSize); ok {
+                                       expectedTasks += len(splits)
+                               } else {
+                                       expectedTasks++
+                               }
+                       }
+
+                       scan := &Scan{}
+                       b.ReportAllocs()
+                       b.ReportMetric(float64(tc.files), "files/op")
+                       b.ResetTimer()
+                       for b.Loop() {
+                               result := scan.finalizeTaskRange(tasks, 0, 
len(tasks), nil, targetSize)
+                               if result.err != nil {
+                                       b.Fatal(result.err)
+                               }
+                               if len(result.tasks) != expectedTasks {
+                                       b.Fatalf("got %d tasks, want %d", 
len(result.tasks), expectedTasks)
+                               }
+                               finalizeTaskRangeSplitBenchmarkSink = 
result.tasks
+                       }
+               })
+       }
+}
diff --git a/table/scan_task_finalization_split_test.go 
b/table/scan_task_finalization_split_test.go
new file mode 100644
index 000000000..953977d4b
--- /dev/null
+++ b/table/scan_task_finalization_split_test.go
@@ -0,0 +1,65 @@
+// 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"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestFinalizeTaskRangeMixedParquetSplitOffsets(t *testing.T) {
+       spec := *iceberg.UnpartitionedSpec
+       valid := finalizationTestDataFile(t, spec, "mem://valid.parquet", nil, 
100, []int64{8, 40, 70})
+       invalid := finalizationTestDataFile(t, spec, "mem://invalid.parquet", 
nil, 100, []int64{8, 40, 40})
+       firstRowID := int64(23)
+
+       tasks := []FileScanTask{
+               {File: invalid, Length: 100},
+               {File: valid, Length: 100, FirstRowID: &firstRowID},
+               {File: valid, Start: 40, Length: 60},
+               {File: invalid, Length: 100},
+               {File: valid, Length: 100, Residual: iceberg.AlwaysTrue{}},
+       }
+       before := append([]FileScanTask(nil), tasks...)
+       result := (&Scan{}).finalizeTaskRange(tasks, 0, len(tasks), nil, 50)
+
+       require.NoError(t, result.err)
+       assert.True(t, result.expanded)
+       require.Len(t, result.tasks, 9)
+       assert.Equal(t, before, tasks, "splitting must not mutate the source 
tasks")
+       assert.Equal(t, before[0], result.tasks[0])
+       assert.Equal(t, before[2], result.tasks[4])
+       assert.Equal(t, before[3], result.tasks[5])
+       for _, first := range []int{1, 6} {
+               for splitIndex, start := range []int64{0, 40, 70} {
+                       split := result.tasks[first+splitIndex]
+                       assert.Equal(t, start, split.Start)
+                       assert.Equal(t, []int64{40, 30, 30}[splitIndex], 
split.Length)
+                       assert.Equal(t, valid, split.File)
+               }
+       }
+       for _, task := range result.tasks[1:4] {
+               assert.Equal(t, &firstRowID, task.FirstRowID)
+       }
+       for _, task := range result.tasks[6:9] {
+               assert.Equal(t, iceberg.AlwaysTrue{}, task.Residual)
+       }
+}

Reply via email to