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 bee737f17 perf(compaction): skip partition work for non-candidate
files (#2276)
bee737f17 is described below
commit bee737f17cdfb8ab1a4a2cfc18a7833903305028
Author: Minh Vu <[email protected]>
AuthorDate: Fri Oct 23 19:04:51 2026 +0200
perf(compaction): skip partition work for non-candidate files (#2276)
* perf(compaction): skip partition work for non-candidate files
Signed-off-by: Minh Vu <[email protected]>
* test(compaction): cover skipped partitions and benchmark large plans
Signed-off-by: Minh Vu <[email protected]>
* test(compaction): cover skipped files in candidate partitions
Signed-off-by: Minh Vu <[email protected]>
---------
Signed-off-by: Minh Vu <[email protected]>
---
table/compaction/compaction.go | 32 +++---
table/compaction/compaction_plan_bench_test.go | 129 +++++++++++++++++++++++++
2 files changed, 145 insertions(+), 16 deletions(-)
diff --git a/table/compaction/compaction.go b/table/compaction/compaction.go
index bf73da220..31c8ba8e5 100644
--- a/table/compaction/compaction.go
+++ b/table/compaction/compaction.go
@@ -201,30 +201,31 @@ func (cfg Config) PlanCompaction(tasks
[]table.FileScanTask) (Plan, error) {
plan.TotalInputBytes += t.File.FileSizeBytes()
}
- // Group tasks by partition key.
+ // Only candidate files need partition keys. Most tables have many
+ // right-sized files, so avoid encoding their partitions and retaining
+ // empty buckets for files the planner will not rewrite.
type partitionBucket struct {
key string
candidates []table.FileScanTask
}
- partitions := make(map[string]partitionBucket)
- var partitionOrder []string
+ partitionIndices := make(map[string]int)
+ var partitionBuckets []partitionBucket
for _, t := range tasks {
- key := partitionBucketKey(t.File.SpecID(),
internal.BorrowedDataFilePartition(t.File))
- bucket, ok := partitions[key]
- if !ok {
- bucket = partitionBucket{key: key}
- partitionOrder = append(partitionOrder, key)
- }
-
if !cfg.isCandidate(t) {
plan.SkippedFiles++
- partitions[key] = bucket
continue
}
- bucket.candidates = append(bucket.candidates, t)
- partitions[key] = bucket
+
+ key := partitionBucketKey(t.File.SpecID(),
internal.BorrowedDataFilePartition(t.File))
+ index, ok := partitionIndices[key]
+ if !ok {
+ index = len(partitionBuckets)
+ partitionIndices[key] = index
+ partitionBuckets = append(partitionBuckets,
partitionBucket{key: key})
+ }
+ partitionBuckets[index].candidates =
append(partitionBuckets[index].candidates, t)
}
// Bin-pack candidates per partition.
@@ -234,8 +235,7 @@ func (cfg Config) PlanCompaction(tasks
[]table.FileScanTask) (Plan, error) {
LargestBinFirst: false,
}
- for _, key := range partitionOrder {
- bucket := partitions[key]
+ for _, bucket := range partitionBuckets {
if len(bucket.candidates) < int(cfg.MinInputFiles) {
plan.SkippedFiles += len(bucket.candidates)
@@ -254,7 +254,7 @@ func (cfg Config) PlanCompaction(tasks
[]table.FileScanTask) (Plan, error) {
}
group := Group{
- PartitionKey: key,
+ PartitionKey: bucket.key,
Tasks: bin,
}
for _, t := range bin {
diff --git a/table/compaction/compaction_plan_bench_test.go
b/table/compaction/compaction_plan_bench_test.go
new file mode 100644
index 000000000..75785a06b
--- /dev/null
+++ b/table/compaction/compaction_plan_bench_test.go
@@ -0,0 +1,129 @@
+// 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 compaction_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/apache/iceberg-go/table"
+ "github.com/apache/iceberg-go/table/compaction"
+ "github.com/stretchr/testify/require"
+)
+
+// Keep the entire plan reachable so the benchmark measures construction of
+// the groups, not just candidate classification.
+var compactionPlanBenchmarkSink compaction.Plan
+
+func TestPlanCompaction_SkippedFilesDoNotChangeCandidateGroups(t *testing.T) {
+ cfg := compaction.DefaultConfig()
+
+ candidateTasks := make([]table.FileScanTask, 0, 12)
+ for i := range 6 {
+ candidateTasks = append(candidateTasks,
+
makeTask(newPartitionedDataFile(fmt.Sprintf("a-%d.parquet", i), 10,
map[int]any{1000: "a"}), 0, 0))
+ candidateTasks = append(candidateTasks,
+
makeTask(newPartitionedDataFile(fmt.Sprintf("b-%d.parquet", i), 10,
map[int]any{1000: "b"}), 0, 0))
+ }
+
+ mixedTasks := make([]table.FileScanTask, 0, len(candidateTasks)*2)
+ for i, task := range candidateTasks {
+ // Mix skipped files in existing partitions and in distinct
+ // partitions that do not contain any candidate files.
+ skipPartition := map[int]any{1000: fmt.Sprintf("unrelated-%d",
i)}
+ if i%3 == 0 {
+ skipPartition = map[int]any{1000: "a"}
+ }
+ mixedTasks = append(mixedTasks,
+
makeTask(newPartitionedDataFile(fmt.Sprintf("skip-%d.parquet", i), 500,
skipPartition), 0, 0),
+ task,
+ )
+ }
+
+ want, err := cfg.PlanCompaction(candidateTasks)
+ require.NoError(t, err)
+ got, err := cfg.PlanCompaction(mixedTasks)
+ require.NoError(t, err)
+
+ require.Equal(t, want.Groups, got.Groups)
+ require.Equal(t, want.EstOutputFiles, got.EstOutputFiles)
+ require.Equal(t, want.EstOutputBytes, got.EstOutputBytes)
+ require.Equal(t, want.SkippedFiles+len(candidateTasks),
got.SkippedFiles)
+ require.Equal(t, len(mixedTasks), got.TotalInputFiles)
+ require.Equal(t,
want.TotalInputBytes+int64(len(candidateTasks))*500*1024*1024,
got.TotalInputBytes)
+}
+
+func TestPlanCompaction_AllSkippedWithManyPartitions(t *testing.T) {
+ cfg := compaction.DefaultConfig()
+ tasks := make([]table.FileScanTask, 128)
+ for i := range tasks {
+ tasks[i] =
makeTask(newPartitionedDataFile(fmt.Sprintf("optimal-%d", i), 500,
+ map[int]any{1: i, 2: "region", 3: true}), 0, 0)
+ }
+
+ plan, err := cfg.PlanCompaction(tasks)
+ require.NoError(t, err)
+ require.Empty(t, plan.Groups)
+ require.Equal(t, len(tasks), plan.TotalInputFiles)
+ require.Equal(t, len(tasks), plan.SkippedFiles)
+}
+
+func BenchmarkPlanCompaction(b *testing.B) {
+ for _, tc := range []struct {
+ files int
+ partitions int
+ candidates int
+ }{
+ {files: 20_000, partitions: 256, candidates: 0},
+ {files: 20_000, partitions: 256, candidates: 10},
+ {files: 20_000, partitions: 256, candidates: 50},
+ {files: 20_000, partitions: 256, candidates: 100},
+ {files: 100_000, partitions: 1_024, candidates: 10},
+ {files: 100_000, partitions: 1_024, candidates: 100},
+ } {
+ b.Run(fmt.Sprintf("files=%d/partitions=%d/candidates=%d%%",
tc.files, tc.partitions, tc.candidates), func(b *testing.B) {
+ // Reuse immutable partition maps as real table
metadata does. All
+ // setup is outside the timer; only the planner is
measured.
+ parts := make([]map[int]any, tc.partitions)
+ for i := range parts {
+ parts[i] = map[int]any{1000: int32(i), 1001:
"us-east", 1002: true, 1003: int64(i) * 60}
+ }
+
+ tasks := make([]table.FileScanTask, tc.files)
+ for i := range tasks {
+ sizeMB := int64(500) // right-sized under the
default 512-MB target
+ if (i*37+i/97)%100 < tc.candidates {
+ sizeMB = 10 // undersized, eligible for
compaction
+ }
+ part := parts[(i*977+i/97)%len(parts)]
+ file :=
newPartitionedDataFile(fmt.Sprintf("file-%d.parquet", i), sizeMB, part)
+ tasks[i] = makeTask(file, 0, 0)
+ }
+
+ cfg := compaction.DefaultConfig()
+ b.ReportAllocs()
+ b.ResetTimer()
+ for b.Loop() {
+ plan, err := cfg.PlanCompaction(tasks)
+ if err != nil {
+ b.Fatal(err)
+ }
+ compactionPlanBenchmarkSink = plan
+ }
+ })
+ }
+}