You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ze...@apache.org on 2026/09/01 03:43:22 UTC

(iceberg-go) branch main updated: perf(table): flatten manifest entries after parallel reads (#1976)

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 877891025 perf(table): flatten manifest entries after parallel reads (#1976)
877891025 is described below

commit 87789102577ce00f050b43fa5eb9f667ad31642a
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Tue Sep 1 05:43:16 2026 +0200

    perf(table): flatten manifest entries after parallel reads (#1976)
    
    * perf(table): flatten manifest entries after parallel reads
    
    Signed-off-by: Minh Vu <vu...@gmail.com>
    
    * fix(table): satisfy lint checks for manifest flattening
    
    Signed-off-by: Minh Vu <vu...@gmail.com>
    
    ---------
    
    Signed-off-by: Minh Vu <vu...@gmail.com>
---
 table/scanner.go                             | 45 +++++++++++++--
 table/scanner_manifest_entries_bench_test.go | 72 +++++++++++++++++-------
 table/scanner_manifest_entries_test.go       | 82 ++++++++++++++++++++++++++++
 3 files changed, 175 insertions(+), 24 deletions(-)

diff --git a/table/scanner.go b/table/scanner.go
index 00a0e4ef4..aec98bfea 100644
--- a/table/scanner.go
+++ b/table/scanner.go
@@ -315,6 +315,41 @@ func (m *manifestEntries) merge(entries []iceberg.ManifestEntry) error {
 	return err
 }
 
+func flattenClassifiedManifestEntries(results []classifiedManifestEntries) *manifestEntries {
+	var counts [manifestEntryKindCount]int
+	for _, result := range results {
+		counts[manifestEntryData] += len(result.dataEntries)
+		counts[manifestEntryPositionalDelete] += len(result.positionalDeleteEntries)
+		counts[manifestEntryEqualityDelete] += len(result.equalityDeleteEntries)
+		counts[manifestEntryDV] += len(result.dvEntries)
+	}
+
+	flattened := &manifestEntries{
+		dataEntries:             make([]iceberg.ManifestEntry, counts[manifestEntryData]),
+		positionalDeleteEntries: make([]iceberg.ManifestEntry, counts[manifestEntryPositionalDelete]),
+		equalityDeleteEntries:   make([]iceberg.ManifestEntry, counts[manifestEntryEqualityDelete]),
+		dvEntries:               make([]iceberg.ManifestEntry, counts[manifestEntryDV]),
+	}
+
+	var offsets [manifestEntryKindCount]int
+	for _, result := range results {
+		offsets[manifestEntryData] += copy(
+			flattened.dataEntries[offsets[manifestEntryData]:], result.dataEntries,
+		)
+		offsets[manifestEntryPositionalDelete] += copy(
+			flattened.positionalDeleteEntries[offsets[manifestEntryPositionalDelete]:], result.positionalDeleteEntries,
+		)
+		offsets[manifestEntryEqualityDelete] += copy(
+			flattened.equalityDeleteEntries[offsets[manifestEntryEqualityDelete]:], result.equalityDeleteEntries,
+		)
+		offsets[manifestEntryDV] += copy(
+			flattened.dvEntries[offsets[manifestEntryDV]:], result.dvEntries,
+		)
+	}
+
+	return flattened
+}
+
 func newPartitionRecord(partitionData map[int]any, partitionType *iceberg.StructType) partitionRecord {
 	out := make(partitionRecord, len(partitionType.FieldList))
 	for i, f := range partitionType.FieldList {
@@ -1053,7 +1088,7 @@ func (scan *Scan) collectManifestEntriesWithSchema(
 	concurrencyLimit := min(scan.concurrency, len(manifestList))
 	manifestIO := newManifestIOBatch(scan.ioF, concurrencyLimit)
 
-	entries := newManifestEntries()
+	manifestResults := make([]classifiedManifestEntries, len(manifestList))
 	g, gctx := errgroup.WithContext(ctx)
 	g.SetLimit(concurrencyLimit)
 
@@ -1061,7 +1096,7 @@ func (scan *Scan) collectManifestEntriesWithSchema(
 		return buildPartitionEvaluator(specID, scan.metadata, schema, partitionFilters, scan.caseSensitive)
 	})
 
-	for _, mf := range manifestList {
+	for manifestIndex, mf := range manifestList {
 		if !scan.checkSequenceNumber(minSeqNum, mf) {
 			continue
 		}
@@ -1079,9 +1114,11 @@ func (scan *Scan) collectManifestEntriesWithSchema(
 			if err != nil {
 				return err
 			}
-			if err := entries.merge(manifestEntries); err != nil {
+			classified, err := classifyManifestEntries(manifestEntries)
+			if err != nil {
 				return err
 			}
+			manifestResults[manifestIndex] = classified
 
 			return nil
 		})
@@ -1091,7 +1128,7 @@ func (scan *Scan) collectManifestEntriesWithSchema(
 		return nil, err
 	}
 
-	return entries, nil
+	return flattenClassifiedManifestEntries(manifestResults), nil
 }
 
 // PlanFiles orchestrates the fetching and filtering of manifests, building a
diff --git a/table/scanner_manifest_entries_bench_test.go b/table/scanner_manifest_entries_bench_test.go
index 42330d7dc..71caf1493 100644
--- a/table/scanner_manifest_entries_bench_test.go
+++ b/table/scanner_manifest_entries_bench_test.go
@@ -60,31 +60,63 @@ func BenchmarkManifestEntryCollection(b *testing.B) {
 		{name: "data", content: iceberg.ManifestContentData},
 		{name: "deletes", content: iceberg.ManifestContentDeletes},
 	} {
-		for _, benchmarkCase := range benchmarkCases {
-			name := fmt.Sprintf("content=%s/manifests=%d/concurrency=%d/entries=%d",
-				workload.name, benchmarkCase.manifestCount, benchmarkCase.concurrency, benchmarkCase.entryCount)
-			b.Run(name, func(b *testing.B) {
-				entries := benchmarkManifestEntries(benchmarkCase.entryCount, workload.content)
-				b.ReportAllocs()
-				b.ResetTimer()
-				b.ReportMetric(float64(benchmarkCase.manifestCount*benchmarkCase.entryCount), "entries/op")
-
-				var result *manifestEntries
-				for range b.N {
-					result = collectManifestEntryBatches(entries, benchmarkCase.manifestCount, benchmarkCase.concurrency)
-				}
-
-				if got, want := len(result.dataEntries)+len(result.positionalDeleteEntries)+
-					len(result.equalityDeleteEntries)+len(result.dvEntries), benchmarkCase.manifestCount*benchmarkCase.entryCount; got != want {
-					b.Fatalf("collected %d entries, want %d", got, want)
-				}
-				runtime.KeepAlive(result)
-			})
+		for _, method := range []struct {
+			name    string
+			collect func([]iceberg.ManifestEntry, int, int) *manifestEntries
+		}{
+			{name: "merge", collect: collectManifestEntryBatchesWithMerge},
+			{name: "flatten", collect: collectManifestEntryBatches},
+		} {
+			for _, benchmarkCase := range benchmarkCases {
+				name := fmt.Sprintf("method=%s/content=%s/manifests=%d/concurrency=%d/entries=%d",
+					method.name, workload.name, benchmarkCase.manifestCount, benchmarkCase.concurrency, benchmarkCase.entryCount)
+				b.Run(name, func(b *testing.B) {
+					entries := benchmarkManifestEntries(benchmarkCase.entryCount, workload.content)
+					b.ReportAllocs()
+					b.ResetTimer()
+					b.ReportMetric(float64(benchmarkCase.manifestCount*benchmarkCase.entryCount), "entries/op")
+
+					var result *manifestEntries
+					for range b.N {
+						result = method.collect(entries, benchmarkCase.manifestCount, benchmarkCase.concurrency)
+					}
+
+					if got, want := len(result.dataEntries)+len(result.positionalDeleteEntries)+
+						len(result.equalityDeleteEntries)+len(result.dvEntries), benchmarkCase.manifestCount*benchmarkCase.entryCount; got != want {
+						b.Fatalf("collected %d entries, want %d", got, want)
+					}
+					runtime.KeepAlive(result)
+				})
+			}
 		}
 	}
 }
 
 func collectManifestEntryBatches(entries []iceberg.ManifestEntry, manifestCount, concurrency int) *manifestEntries {
+	results := make([]classifiedManifestEntries, manifestCount)
+	var g errgroup.Group
+	g.SetLimit(min(concurrency, manifestCount))
+	for manifestIndex := range manifestCount {
+		g.Go(func() error {
+			manifestEntries := append(make([]iceberg.ManifestEntry, 0, len(entries)), entries...)
+			classified, err := classifyManifestEntries(manifestEntries)
+			if err != nil {
+				return err
+			}
+			results[manifestIndex] = classified
+
+			return nil
+		})
+	}
+
+	if err := g.Wait(); err != nil {
+		panic(err)
+	}
+
+	return flattenClassifiedManifestEntries(results)
+}
+
+func collectManifestEntryBatchesWithMerge(entries []iceberg.ManifestEntry, manifestCount, concurrency int) *manifestEntries {
 	collected := newManifestEntries()
 	var g errgroup.Group
 	g.SetLimit(min(concurrency, manifestCount))
diff --git a/table/scanner_manifest_entries_test.go b/table/scanner_manifest_entries_test.go
new file mode 100644
index 000000000..10f7cb66c
--- /dev/null
+++ b/table/scanner_manifest_entries_test.go
@@ -0,0 +1,82 @@
+// 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"
+)
+
+func TestFlattenClassifiedManifestEntriesPreservesManifestOrder(t *testing.T) {
+	snapshotID := int64(1)
+	dataOne := iceberg.NewManifestEntry(iceberg.EntryStatusADDED, &snapshotID, nil, nil, &mockDataFile{
+		path:        "data-1.parquet",
+		contentType: iceberg.EntryContentData,
+	})
+	dataTwo := iceberg.NewManifestEntry(iceberg.EntryStatusADDED, &snapshotID, nil, nil, &mockDataFile{
+		path:        "data-2.parquet",
+		contentType: iceberg.EntryContentData,
+	})
+	positionalDelete := iceberg.NewManifestEntry(iceberg.EntryStatusADDED, &snapshotID, nil, nil, &mockDataFile{
+		path:        "pos-delete.parquet",
+		contentType: iceberg.EntryContentPosDeletes,
+	})
+	equalityDelete := iceberg.NewManifestEntry(iceberg.EntryStatusADDED, &snapshotID, nil, nil, &mockDataFile{
+		path:        "eq-delete.parquet",
+		contentType: iceberg.EntryContentEqDeletes,
+	})
+	dv := iceberg.NewManifestEntry(iceberg.EntryStatusADDED, &snapshotID, nil, nil, &mockDataFile{
+		path:        "deletion-vector.puffin",
+		contentType: iceberg.EntryContentPosDeletes,
+		format:      iceberg.PuffinFile,
+	})
+
+	got := flattenClassifiedManifestEntries([]classifiedManifestEntries{
+		{
+			dataEntries:           []iceberg.ManifestEntry{dataOne},
+			equalityDeleteEntries: []iceberg.ManifestEntry{equalityDelete},
+			dvEntries:             []iceberg.ManifestEntry{dv},
+		},
+		{},
+		{dataEntries: []iceberg.ManifestEntry{dataTwo}, positionalDeleteEntries: []iceberg.ManifestEntry{positionalDelete}},
+	})
+
+	assert.Equal(t, []iceberg.ManifestEntry{dataOne, dataTwo}, got.dataEntries)
+	assert.Equal(t, []iceberg.ManifestEntry{positionalDelete}, got.positionalDeleteEntries)
+	assert.Equal(t, []iceberg.ManifestEntry{equalityDelete}, got.equalityDeleteEntries)
+	assert.Equal(t, []iceberg.ManifestEntry{dv}, got.dvEntries)
+	assert.Equal(t, len(got.dataEntries), cap(got.dataEntries))
+	assert.Equal(t, len(got.positionalDeleteEntries), cap(got.positionalDeleteEntries))
+	assert.Equal(t, len(got.equalityDeleteEntries), cap(got.equalityDeleteEntries))
+	assert.Equal(t, len(got.dvEntries), cap(got.dvEntries))
+}
+
+func TestFlattenClassifiedManifestEntriesEmpty(t *testing.T) {
+	got := flattenClassifiedManifestEntries(nil)
+
+	assert.NotNil(t, got.dataEntries)
+	assert.NotNil(t, got.positionalDeleteEntries)
+	assert.NotNil(t, got.equalityDeleteEntries)
+	assert.NotNil(t, got.dvEntries)
+	assert.Empty(t, got.dataEntries)
+	assert.Empty(t, got.positionalDeleteEntries)
+	assert.Empty(t, got.equalityDeleteEntries)
+	assert.Empty(t, got.dvEntries)
+}