Skip to content

Commit 25b1a81

Browse files
perf(table): compile metrics modes once (#1991)
* perf(table): compile metrics modes once * fix(table): address metrics plan review feedback Signed-off-by: Minh Vu <vuhoangminh97@gmail.com> --------- Signed-off-by: Minh Vu <vuhoangminh97@gmail.com>
1 parent 6624765 commit 25b1a81

3 files changed

Lines changed: 349 additions & 90 deletions

File tree

table/arrow_utils.go

Lines changed: 126 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1664,66 +1664,33 @@ func must[T any](v T, err error) T {
16641664
}
16651665

16661666
type arrowStatsCollector struct {
1667-
fieldID int
1668-
schema *iceberg.Schema
1669-
props iceberg.Properties
1670-
defaultMode string
1667+
fieldID int
1668+
schema *iceberg.Schema
1669+
defaultMode tblutils.MetricsMode
1670+
defaultModeError error
1671+
columnModes map[string]tblutils.MetricsMode
1672+
columnModeErrors map[string]error
16711673
}
16721674

1673-
func (a *arrowStatsCollector) Schema(_ *iceberg.Schema, results func() []tblutils.StatisticsCollector) []tblutils.StatisticsCollector {
1674-
return results()
1675-
}
1676-
1677-
func (a *arrowStatsCollector) Struct(_ iceberg.StructType, results []func() []tblutils.StatisticsCollector) []tblutils.StatisticsCollector {
1678-
result := make([]tblutils.StatisticsCollector, 0, len(results))
1679-
for _, res := range results {
1680-
result = append(result, res()...)
1675+
func (a *arrowStatsCollector) resolveColumnMetricsMode(colName string) tblutils.MetricsMode {
1676+
if a.defaultModeError != nil {
1677+
panic(a.defaultModeError)
16811678
}
16821679

1683-
return result
1684-
}
1685-
1686-
func (a *arrowStatsCollector) Field(field iceberg.NestedField, fieldRes func() []tblutils.StatisticsCollector) []tblutils.StatisticsCollector {
1687-
a.fieldID = field.ID
1688-
1689-
return fieldRes()
1690-
}
1691-
1692-
func (a *arrowStatsCollector) List(list iceberg.ListType, elemResult func() []tblutils.StatisticsCollector) []tblutils.StatisticsCollector {
1693-
a.fieldID = list.ElementID
1694-
1695-
return elemResult()
1696-
}
1697-
1698-
func (a *arrowStatsCollector) Map(m iceberg.MapType, keyResult, valResult func() []tblutils.StatisticsCollector) []tblutils.StatisticsCollector {
1699-
a.fieldID = m.KeyID
1700-
keyRes := keyResult()
1701-
1702-
a.fieldID = m.ValueID
1703-
valRes := valResult()
1704-
1705-
return append(keyRes, valRes...)
1706-
}
1707-
1708-
func (a *arrowStatsCollector) resolveColumnMetricsMode(colName string) tblutils.MetricsMode {
1709-
metMode, err := tblutils.MatchMetricsMode(a.defaultMode)
1710-
if err != nil {
1711-
panic(err)
1680+
if metMode, ok := a.columnModes[colName]; ok {
1681+
return metMode
17121682
}
1713-
if colMode, ok := a.props[MetricsModeColumnConfPrefix+"."+colName]; ok {
1714-
metMode, err = tblutils.MatchMetricsMode(colMode)
1715-
if err != nil {
1716-
panic(err)
1717-
}
1683+
if err, ok := a.columnModeErrors[colName]; ok {
1684+
panic(err)
17181685
}
17191686

1720-
return metMode
1687+
return a.defaultMode
17211688
}
17221689

1723-
func (a *arrowStatsCollector) Primitive(dt iceberg.PrimitiveType) []tblutils.StatisticsCollector {
1690+
func (a *arrowStatsCollector) primitiveCollector(dt iceberg.PrimitiveType, isNested bool) (tblutils.StatisticsCollector, bool) {
17241691
colName, ok := a.schema.FindColumnName(a.fieldID)
17251692
if !ok {
1726-
return []tblutils.StatisticsCollector{}
1693+
return tblutils.StatisticsCollector{}, false
17271694
}
17281695

17291696
metMode := a.resolveColumnMetricsMode(colName)
@@ -1737,47 +1704,141 @@ func (a *arrowStatsCollector) Primitive(dt iceberg.PrimitiveType) []tblutils.Sta
17371704
}
17381705
}
17391706

1740-
isNested := strings.Contains(colName, ".")
17411707
if isNested && (metMode.Typ == tblutils.MetricModeTruncate || metMode.Typ == tblutils.MetricModeFull) {
17421708
metMode = tblutils.MetricsMode{Typ: tblutils.MetricModeCounts}
17431709
}
17441710

1745-
return []tblutils.StatisticsCollector{{
1711+
return tblutils.StatisticsCollector{
17461712
FieldID: a.fieldID,
17471713
IcebergTyp: dt,
17481714
ColName: colName,
17491715
Mode: metMode,
1750-
}}
1716+
}, true
17511717
}
17521718

1753-
func (a *arrowStatsCollector) Variant(_ iceberg.VariantType) []tblutils.StatisticsCollector {
1719+
func (a *arrowStatsCollector) variantCollector() (tblutils.StatisticsCollector, bool) {
17541720
colName, ok := a.schema.FindColumnName(a.fieldID)
17551721
if !ok {
1756-
return []tblutils.StatisticsCollector{}
1722+
return tblutils.StatisticsCollector{}, false
17571723
}
17581724

1759-
return []tblutils.StatisticsCollector{{
1725+
return tblutils.StatisticsCollector{
17601726
FieldID: a.fieldID,
17611727
ColName: colName,
17621728
Mode: a.resolveColumnMetricsMode(colName),
1763-
}}
1729+
}, true
1730+
}
1731+
1732+
func statsPlanFieldCount(field iceberg.NestedField) int {
1733+
switch typ := field.Type.(type) {
1734+
case *iceberg.StructType:
1735+
count := 0
1736+
for _, nestedField := range typ.FieldList {
1737+
count += statsPlanFieldCount(nestedField)
1738+
}
1739+
1740+
return count
1741+
case *iceberg.ListType:
1742+
return statsPlanFieldCount(typ.ElementField())
1743+
case *iceberg.MapType:
1744+
return statsPlanFieldCount(typ.KeyField()) + statsPlanFieldCount(typ.ValueField())
1745+
default:
1746+
return 1
1747+
}
1748+
}
1749+
1750+
func collectStatsPlanField(visitor *arrowStatsCollector, result map[int]tblutils.StatisticsCollector, field iceberg.NestedField, isNested bool) {
1751+
switch typ := field.Type.(type) {
1752+
case *iceberg.StructType:
1753+
for _, nestedField := range typ.FieldList {
1754+
collectStatsPlanField(visitor, result, nestedField, true)
1755+
}
1756+
case *iceberg.ListType:
1757+
collectStatsPlanField(visitor, result, typ.ElementField(), true)
1758+
case *iceberg.MapType:
1759+
collectStatsPlanField(visitor, result, typ.KeyField(), true)
1760+
collectStatsPlanField(visitor, result, typ.ValueField(), true)
1761+
case iceberg.VariantType:
1762+
visitor.fieldID = field.ID
1763+
if collector, ok := visitor.variantCollector(); ok {
1764+
result[collector.FieldID] = collector
1765+
}
1766+
default:
1767+
visitor.fieldID = field.ID
1768+
collector, ok := visitor.primitiveCollector(field.Type.(iceberg.PrimitiveType), isNested)
1769+
if ok {
1770+
result[collector.FieldID] = collector
1771+
}
1772+
}
17641773
}
17651774

1766-
func computeStatsPlan(sc *iceberg.Schema, props iceberg.Properties) (map[int]tblutils.StatisticsCollector, error) {
1767-
result := make(map[int]tblutils.StatisticsCollector)
1775+
func computeStatsPlan(sc *iceberg.Schema, props iceberg.Properties) (result map[int]tblutils.StatisticsCollector, err error) {
1776+
defer func() {
1777+
if r := recover(); r != nil {
1778+
result = nil
1779+
switch e := r.(type) {
1780+
case string:
1781+
err = fmt.Errorf("%w: %s", iceberg.ErrInvalidSchema, e)
1782+
case error:
1783+
err = fmt.Errorf("error encountered during schema visitor: %w", e)
1784+
}
1785+
}
1786+
}()
1787+
1788+
if sc == nil {
1789+
return nil, fmt.Errorf("%w: cannot visit nil schema", iceberg.ErrInvalidArgument)
1790+
}
1791+
1792+
defaultMode, defaultModeErr := tblutils.MatchMetricsMode(
1793+
props.Get(DefaultWriteMetricsModeKey, DefaultWriteMetricsModeDefault))
1794+
overrideCount := 0
1795+
for key := range props {
1796+
if strings.HasPrefix(key, MetricsModeColumnConfPrefix+".") {
1797+
overrideCount++
1798+
}
1799+
}
1800+
1801+
var columnModes map[string]tblutils.MetricsMode
1802+
var columnModeErrors map[string]error
1803+
for key, rawMode := range props {
1804+
colName, ok := strings.CutPrefix(key, MetricsModeColumnConfPrefix+".")
1805+
if !ok {
1806+
continue
1807+
}
1808+
1809+
mode, err := tblutils.MatchMetricsMode(rawMode)
1810+
if err != nil {
1811+
if columnModeErrors == nil {
1812+
columnModeErrors = make(map[string]error, overrideCount)
1813+
}
1814+
1815+
columnModeErrors[colName] = err
1816+
1817+
continue
1818+
}
1819+
if columnModes == nil {
1820+
columnModes = make(map[string]tblutils.MetricsMode, overrideCount)
1821+
}
1822+
columnModes[colName] = mode
1823+
}
17681824

17691825
visitor := &arrowStatsCollector{
1770-
schema: sc, props: props,
1771-
defaultMode: props.Get(DefaultWriteMetricsModeKey, DefaultWriteMetricsModeDefault),
1826+
schema: sc,
1827+
defaultMode: defaultMode,
1828+
defaultModeError: defaultModeErr,
1829+
columnModes: columnModes,
1830+
columnModeErrors: columnModeErrors,
17721831
}
17731832

1774-
collectors, err := iceberg.PreOrderVisit(sc, visitor)
1775-
if err != nil {
1776-
return nil, err
1833+
fields := sc.FieldsRef(internal.SchemaRef{})
1834+
resultCount := 0
1835+
for _, field := range fields {
1836+
resultCount += statsPlanFieldCount(field)
17771837
}
17781838

1779-
for _, entry := range collectors {
1780-
result[entry.FieldID] = entry
1839+
result = make(map[int]tblutils.StatisticsCollector, resultCount)
1840+
for _, field := range fields {
1841+
collectStatsPlanField(visitor, result, field, strings.Contains(field.Name, "."))
17811842
}
17821843

17831844
return result, nil

table/arrow_utils_bench_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package table
19+
20+
import (
21+
"fmt"
22+
"testing"
23+
24+
"github.com/apache/iceberg-go"
25+
)
26+
27+
func BenchmarkComputeStatsPlan(b *testing.B) {
28+
for _, fieldCount := range []int{100, 1000, 10000} {
29+
for _, benchmarkCase := range []struct {
30+
name string
31+
defaultMode string
32+
overrideStride int
33+
unrelatedProperties int
34+
}{
35+
{name: "default", defaultMode: "truncate(16)"},
36+
{name: "one_percent_overrides", defaultMode: "truncate(16)", overrideStride: 100},
37+
{name: "one_percent_overrides_many_properties", defaultMode: "truncate(16)", overrideStride: 100, unrelatedProperties: 1000},
38+
} {
39+
b.Run(fmt.Sprintf("fields=%d/%s", fieldCount, benchmarkCase.name), func(b *testing.B) {
40+
schema := benchmarkMetricsSchema(fieldCount)
41+
props := iceberg.Properties{DefaultWriteMetricsModeKey: benchmarkCase.defaultMode}
42+
for i := 0; benchmarkCase.overrideStride > 0 && i < fieldCount; i += benchmarkCase.overrideStride {
43+
props[MetricsModeColumnConfPrefix+fmt.Sprintf(".field_%d", i)] = "counts"
44+
}
45+
for i := range benchmarkCase.unrelatedProperties {
46+
props[fmt.Sprintf("unrelated.property.%d", i)] = "value"
47+
}
48+
49+
b.ReportAllocs()
50+
b.ResetTimer()
51+
for range b.N {
52+
plan, err := computeStatsPlan(schema, props)
53+
if err != nil {
54+
b.Fatal(err)
55+
}
56+
if len(plan) != fieldCount {
57+
b.Fatalf("expected %d stats columns, got %d", fieldCount, len(plan))
58+
}
59+
}
60+
})
61+
}
62+
}
63+
}
64+
65+
func benchmarkMetricsSchema(fieldCount int) *iceberg.Schema {
66+
fields := make([]iceberg.NestedField, fieldCount)
67+
for i := range fields {
68+
fields[i] = iceberg.NestedField{
69+
ID: i + 1,
70+
Name: fmt.Sprintf("field_%d", i),
71+
Type: iceberg.PrimitiveTypes.String,
72+
Required: true,
73+
}
74+
}
75+
76+
return iceberg.NewSchema(0, fields...)
77+
}

0 commit comments

Comments
 (0)