Skip to content

perf(spark): stop deep schema comparisons per record in partial-update merging - #20269

Draft
yihua wants to merge 1 commit into
apache:masterfrom
yihua:perf-partial-merge-schema-lookup
Draft

yihua wants to merge 1 commit into
apache:masterfrom
yihua:perf-partial-merge-schema-lookup

Conversation

@yihua

@yihua yihua commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

closes #20268

Merging a partial update into a full record built the merged schema by converting the Spark struct type back to a HoodieSchema, which drops field defaults (for example the -1 default of the position-merging row index column). That schema is unequal to the reader schema but has the same Avro hash, so every schema-keyed lookup on the merge path (LocalHoodieSchemaCache, HoodieInternalRowUtils.getCachedSchema, the merged-schema cache, isPartial) compared the two field by field once per merged record. #12949 removed this class of cost by interning schemas; interning does not help here because the two schemas are not equal.

Summary and Changelog

When the merged fields are all the reader fields, SparkRecordMergingUtils.getCachedMergedSchema now returns the reader schema itself; partial-only merges are unchanged. HoodieSchema.hashCode delegates to the Avro schema's cached hash instead of Objects.hash. New TestSparkRecordMergingUtils covers the full-plus-partial, full-update and partial-plus-partial merges (two cases fail on master).

Impact

Lower CPU and allocation when reading or compacting MOR file groups with partial-update log records on Spark. Single-thread merge benchmark (31-field reader schema, 2-column partial records): 2367 to 486 ns and 13.8 KB to 0.7 KB allocated per merged record. Merged values are unchanged.

Risk Level

low. A full newer record is now returned as is instead of copied, so in event-time ordering deltaMerge returns empty (keep the buffered record) when the buffered full record wins; TestBufferedRecordMerger is updated for that. The kept record has the same values, ordering value and delete flag, and keeps its own operation and key. TestPartialUpdateForMergeInto passes.

Documentation Update

none

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable

…e merging

When a partial update is merged into a record carrying every reader field,
SparkRecordMergingUtils built the merged schema by converting the merged
struct type back to a HoodieSchema. That conversion drops field defaults
(for example the -1 default of the position-merging row index column), so
the merged schema is unequal to the reader schema while Avro gives both the
same hash. Every schema-keyed lookup holding both (the record context schema
cache, HoodieInternalRowUtils.getCachedSchema, the merged-schema cache,
isPartial) then compared the two schemas field by field, once per merged
record.

Reuse the reader schema as the merged schema when all reader fields are
present, and let HoodieSchema.hashCode delegate to the Avro schema's cached
hash code.
@github-actions github-actions Bot added the size:M PR with lines of changes in (100, 300] label Oct 9, 2026
@codecov-commenter

codecov-commenter commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.63%. Comparing base (308f78e) to head (d606f5e).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #20269      +/-   ##
============================================
+ Coverage     80.62%   80.63%   +0.01%     
- Complexity    35055    35062       +7     
============================================
  Files          2552     2552              
  Lines        143400   143402       +2     
  Branches      17454    17455       +1     
============================================
+ Hits         115615   115631      +16     
+ Misses        19867    19856      -11     
+ Partials       7918     7915       -3     
Components Coverage Δ
hudi-common 84.18% <100.00%> (+0.02%) ⬆️
hudi-client 83.82% <100.00%> (+0.01%) ⬆️
hudi-flink 85.98% <ø> (+0.01%) ⬆️
hudi-spark-datasource 74.05% <ø> (-0.01%) ⬇️
hudi-utilities 78.15% <ø> (-0.04%) ⬇️
hudi-cli 70.80% <ø> (ø)
hudi-hadoop 71.11% <ø> (ø)
hudi-sync 76.25% <ø> (+0.07%) ⬆️
hudi-io 81.52% <ø> (+0.09%) ⬆️
hudi-timeline-service 83.41% <ø> (ø)
hudi-cloud 81.00% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 52.42% <20.00%> (+<0.01%) ⬆️
flink-integration-tests 49.54% <100.00%> (+<0.01%) ⬆️
hadoop-mr-java-client 43.99% <100.00%> (+0.01%) ⬆️
integration-tests 13.45% <20.00%> (-0.01%) ⬇️
spark-client-hadoop-common 38.74% <100.00%> (+0.05%) ⬆️
spark-java-tests 52.69% <60.00%> (+<0.01%) ⬆️
spark-scala-tests 47.51% <100.00%> (+<0.01%) ⬆️
utilities 36.89% <20.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...org/apache/hudi/merge/SparkRecordMergingUtils.java 97.18% <100.00%> (+0.08%) ⬆️
...va/org/apache/hudi/common/schema/HoodieSchema.java 87.42% <100.00%> (ø)

... and 18 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M PR with lines of changes in (100, 300]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Partial-update merging compares Avro schemas field by field for every record

2 participants