This is an automated email from the ASF dual-hosted git repository. voonhous pushed a commit to branch release-1.2.1 in repository https://gitbox.apache.org/repos/asf/hudi.git
commit c14a871d57e80493cc9e336a044a25c6000d977b Author: voonhous <[email protected]> AuthorDate: Thu Sep 17 18:45:36 2026 +0800 perf(common): cache nested Avro record class lookup (#19972) #19956 replaced SpecificData#getClass with an uncached Class.forName per nested record, dropping the class cache Avro keeps by schema full name. Delete block reads hit it twice per delete record (the record and its ordering-value wrapper), costing ~450-880 ns per lookup vs ~3-5 ns before. Cache the resolved class by schema full name, as SpecificData does. (cherry picked from commit 5bccaabf56b60a0efd9665b5b6c9d115993f1e8e) --- .../java/org/apache/hudi/avro/HoodieAvroUtils.java | 24 ++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/hudi-common/src/main/java/org/apache/hudi/avro/HoodieAvroUtils.java b/hudi-common/src/main/java/org/apache/hudi/avro/HoodieAvroUtils.java index d97fe8fdcb60..28d08c25ae86 100644 --- a/hudi-common/src/main/java/org/apache/hudi/avro/HoodieAvroUtils.java +++ b/hudi-common/src/main/java/org/apache/hudi/avro/HoodieAvroUtils.java @@ -98,6 +98,7 @@ import java.util.Properties; import java.util.Set; import java.util.TimeZone; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import static org.apache.avro.Schema.Type.ARRAY; @@ -124,6 +125,13 @@ public class HoodieAvroUtils { private static final Properties PROPERTIES = new Properties(); + /** + * Generated SpecificRecord classes keyed by schema full name, mirroring the class cache inside + * {@link SpecificData#getClass(Schema)} that the direct {@code Class.forName} lookup in + * {@link #getSpecificRecordClass(Schema, SpecificData)} bypasses. Bounded because only compiled SCHEMA$ schemas reach it. + */ + private static final Map<String, Class<? extends SpecificRecordBase>> SPECIFIC_RECORD_CLASS_CACHE = new ConcurrentHashMap<>(); + /** * Resolves the Avro library version, preferring Maven's generated pom.properties over * {@link Package#getImplementationVersion()}. The latter comes from whatever manifest happens to @@ -1563,14 +1571,18 @@ public class HoodieAvroUtils { * {@link SpecificData#getClass(Schema)}), which rejects Hudi's generated classes. * * <p>Only pass schemas taken from a compiled SCHEMA$, never a schema read from storage, which is what the validation guards against. + * + * <p>The result is cached by schema full name so the per-record path does not pay {@code Class.forName}. */ private static Class<? extends SpecificRecordBase> getSpecificRecordClass(Schema recordSchema, SpecificData specificData) { - String className = SpecificData.getClassName(recordSchema); - try { - return Class.forName(className, false, specificData.getClassLoader()).asSubclass(SpecificRecordBase.class); - } catch (ClassNotFoundException e) { - throw new HoodieException("Failed to load SpecificRecord class " + className + " for Avro schema " + recordSchema.getFullName(), e); - } + return SPECIFIC_RECORD_CLASS_CACHE.computeIfAbsent(recordSchema.getFullName(), fullName -> { + String className = SpecificData.getClassName(recordSchema); + try { + return Class.forName(className, false, specificData.getClassLoader()).asSubclass(SpecificRecordBase.class); + } catch (ClassNotFoundException e) { + throw new HoodieException("Failed to load SpecificRecord class " + className + " for Avro schema " + fullName, e); + } + }); } private static Object convertFieldToSpecificRecordValue(Schema fieldSchema, Object value, SpecificData specificData) {
