laskoviymishka commented on code in PR #2118:
URL: https://github.com/apache/iceberg-go/pull/2118#discussion_r4193433513


##########
table/arrow_scanner_bench_test.go:
##########
@@ -276,6 +277,11 @@ func benchmarkSplitParquetScan(b *testing.B) (Metadata, 
iceio.IO, []FileScanTask
        dataFile := dataFileBuilder.SplitOffsets(offsets).Build()
        fullTask := FileScanTask{File: dataFile, Start: 0, Length: 
int64(buf.Len())}
        splitTasks, split := splitParquetScanTask(fullTask, 1)
+       if rowGroups == 1 && !split {

Review Comment:
   A one-line comment would help, since it isn't obvious why `Length--` is 
here. A single row group can't be split (`len(offsets) < 2`), so this 
fabricates a partial task to force the `RangeSet`/`PlanningSplitOffsets` path. 
The `expected one split task per row group` fatal just below also no longer 
describes the `rowGroups==1` case, so worth a tweak.



##########
internal/data_file_ref_test.go:
##########
@@ -0,0 +1,102 @@
+// 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 internal
+
+import (
+       "slices"
+       "testing"
+)
+
+type splitOffsetsCollectionsTestFile struct {
+       offsets              []int64
+       splitOffsetsCalls    int
+       otherCollectionCalls int
+}
+
+func (f *splitOffsetsCollectionsTestFile) ColumnSizes() map[int]int64 {
+       f.otherCollectionCalls++
+
+       return nil
+}
+
+func (f *splitOffsetsCollectionsTestFile) KeyMetadata() []byte {
+       f.otherCollectionCalls++
+
+       return nil
+}
+
+func (f *splitOffsetsCollectionsTestFile) SplitOffsets() []int64 {
+       f.splitOffsetsCalls++
+
+       return f.offsets
+}
+
+func (f *splitOffsetsCollectionsTestFile) EqualityFieldIDs() []int {
+       f.otherCollectionCalls++
+
+       return nil
+}
+
+type splitOffsetsReferenceTestFile struct {
+       splitOffsetsCollectionsTestFile
+       borrowedOffsets []int64
+       refCalls        int
+}
+
+func (f *splitOffsetsReferenceTestFile) DataFileCollectionsRef(DataFileRef) (
+       map[int]int64, []byte, []int64, []int,
+) {
+       f.refCalls++
+
+       return nil, nil, f.borrowedOffsets, nil
+}
+
+func TestBorrowedDataFileSplitOffsetsUsesPublicFallbackOnly(t *testing.T) {
+       want := []int64{12, 40, 96}
+       file := &splitOffsetsCollectionsTestFile{offsets: want}
+
+       got := BorrowedDataFileSplitOffsets(file)
+       if !slices.Equal(got, want) {
+               t.Fatalf("BorrowedDataFileSplitOffsets() = %v, want %v", got, 
want)
+       }
+       if file.splitOffsetsCalls != 1 {
+               t.Fatalf("SplitOffsets() called %d times, want 1", 
file.splitOffsetsCalls)
+       }
+       if file.otherCollectionCalls != 0 {
+               t.Fatalf("other collection getters called %d times, want 0", 
file.otherCollectionCalls)
+       }
+}
+
+func TestBorrowedDataFileSplitOffsetsUsesBorrowedReference(t *testing.T) {
+       borrowed := []int64{12, 40, 96}
+       file := &splitOffsetsReferenceTestFile{borrowedOffsets: borrowed}
+
+       got := BorrowedDataFileSplitOffsets(file)
+       if !slices.Equal(got, borrowed) {
+               t.Fatalf("BorrowedDataFileSplitOffsets() = %v, want %v", got, 
borrowed)
+       }
+       if len(got) > 0 && &got[0] != &borrowed[0] {

Review Comment:
   Both new tests drive hand-written fakes that return whatever they're handed, 
so this identity check proves the helper dispatches, not that the real 
`*dataFile` borrows. The allocation win rests entirely on 
`*dataFile.DataFileCollectionsRef` returning its backing `Splits` array without 
copying, and nothing here exercises that path. If it clones, the win silently 
disappears and CI stays green.
   
   I'd add a test in the root or `table` package that builds a real `DataFile`, 
calls `BorrowedDataFileSplitOffsets` on it twice and asserts `&a[0] == &b[0]`, 
plus asserts `df.SplitOffsets()` hands back a different backing array. Making 
the two existing cases table-driven over `nil` / `[]int64{}` / populated would 
cover the empty paths cheaply while you're in here.



##########
table/arrow_scanner.go:
##########
@@ -2035,7 +2035,7 @@ func (as *arrowScan) processRecordsWithPlans(
                        (task.Value.Length != 0 && task.Value.Length != 
task.Value.File.FileSizeBytes()) {
                        tester.RangeSet = true
                        tester.Start, tester.Length = 
adjustParquetTaskRange(task.Value, rdr.SourceFileSize())
-                       tester.PlanningSplitOffsets = 
task.Value.File.SplitOffsets()
+                       tester.PlanningSplitOffsets = 
iceinternal.BorrowedDataFileSplitOffsets(task.Value.File)

Review Comment:
   This stores the borrowed slice in `tester.PlanningSplitOffsets`, which 
outlives the call and is shared across the split tasks fanned out from one 
file. Every consumer today is read-only so there's no bug, but the field's doc 
doesn't say it's now a borrowed, read-only view of the `DataFile`'s internal 
state.
   
   A later in-place `slices.Sort`/`Insert`/`append` on it would corrupt 
manifest-derived state and race with the other concurrent tasks sharing that 
file. I'd restate the read-only contract on the `PlanningSplitOffsets` field 
(and/or right here at the assignment), and capping with a three-index slice 
(`...[:n:n]`) is a free way to shut the append-aliasing path.



##########
table/scan_splits_bench_test.go:
##########
@@ -0,0 +1,90 @@
+// 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 splitRemoteScanTasksBenchmarkSink []FileScanTask
+
+func BenchmarkSplitRemoteScanTasksSplitOffsets(b *testing.B) {
+       const filesPerOp = 1024
+
+       for _, offsetCount := range []int{1, 32, 128, 512} {
+               for _, publicGetter := range []bool{false, true} {
+                       b.Run(fmt.Sprintf("offsets=%d/public=%t", offsetCount, 
publicGetter), func(b *testing.B) {
+                               fileSize := int64(offsetCount+1) * 64
+                               offsets := make([]int64, offsetCount)
+                               for i := range offsets {
+                                       offsets[i] = int64(i+1) * 64
+                               }
+
+                               dataFileBuilder, err := 
iceberg.NewDataFileBuilder(
+                                       *iceberg.UnpartitionedSpec,
+                                       iceberg.EntryContentData,
+                                       "mem://benchmark/split-offsets.parquet",
+                                       iceberg.ParquetFile,
+                                       nil,
+                                       nil,
+                                       nil,
+                                       1,
+                                       fileSize,
+                               )
+                               if err != nil {
+                                       b.Fatal(err)
+                               }
+                               file := 
dataFileBuilder.SplitOffsets(offsets).Build()
+                               if publicGetter {
+                                       file = 
splitOffsetsPublicBenchmarkFile{DataFile: file}
+                               }
+                               tasks := make([]FileScanTask, filesPerOp)
+                               for i := range tasks {
+                                       tasks[i] = FileScanTask{File: file, 
Start: 0, Length: fileSize}
+                               }
+
+                               wantTasksPerFile := 1
+                               if offsetCount > 1 {
+                                       splits, ok := 
splitParquetScanTask(tasks[0], fileSize/2)
+                                       if !ok {
+                                               b.Fatal("expected split offsets 
to produce scan tasks")
+                                       }
+                                       wantTasksPerFile = len(splits)
+                               }
+                               wantTasks := filesPerOp * wantTasksPerFile
+
+                               b.ReportAllocs()
+                               b.ResetTimer()
+                               for b.Loop() {
+                                       result := splitRemoteScanTasks(tasks, 
fileSize/2)
+                                       if len(result) != wantTasks {
+                                               
b.Fatalf("splitRemoteScanTasks() returned %d tasks, want %d", len(result), 
wantTasks)
+                                       }
+                                       splitRemoteScanTasksBenchmarkSink = 
result
+                               }
+                               b.ReportMetric(filesPerOp, "files/op")
+                               b.ReportMetric(float64(offsetCount), 
"split_offsets/file")
+                       })
+               }
+       }
+}
+
+type splitOffsetsPublicBenchmarkFile struct{ iceberg.DataFile }

Review Comment:
   Worth a one-line comment that this embeds the `iceberg.DataFile` interface 
on purpose: that promotes only the public methods, so `DataFileCollectionsRef` 
isn't visible, the type assertion fails, and the clone fallback runs, which is 
the baseline this benchmark wants. The name alone doesn't convey that the 
embedding is what hides the fast path.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to