perf: cache Iceberg reflection lookups on the planning path - #5222
Open
andygrove wants to merge 3 commits into
Open
perf: cache Iceberg reflection lookups on the planning path#5222andygrove wants to merge 3 commits into
andygrove wants to merge 3 commits into
Conversation
`Class.getMethod` linearly scans a class's public methods and returns a fresh `Method` copy per call, and Comet's Iceberg paths resolve the same handful of accessors once per file scan task, per partition field and per delete file. `extractFileLocation` additionally probes for `location()` on every file, so on Iceberg versions that only have `path()` each call built a `NoSuchMethodException`. Resolve lookups through a cache in `IcebergReflection`, keyed on the class via `ClassValue` so entries die with the class rather than pinning a classloader. Misses are cached too. Memoize the per-task field-id mapping and hoist the loop-invariant schema comparison out of the serialization loop.
Fold `setAccessible` into `findMethod` so accessibility is a property of the resolved method rather than of the call site, dropping the `findAccessibleMethod` / `getAccessibleMethod` pair and its cache-key namespace. Share the declared-method resolution between `getDeclaredMethod` and `findMethodInHierarchy`, and read the cache before `computeIfAbsent` so a hit does not allocate a mapping function. Drop the now-unused single-argument `getFileFormat`, and give `getEqualityFieldIds` the same class-taking shape so its callers stop reloading `DeleteFile` per delete file. In the serde, take the `PartitionSpecParser.toJson` accessor as a plain `Option[Method]` instead of a by-name parameter backed by a `lazy val`, and reuse the scan-schema field-id mapping the metadata already carries.
Contributor
|
I'll take a look at this today. I recall our concerns about this the last time it was attempted was maintaining the semantics of: reflection failures during CometScanRule should just trigger a fall back with a message. Reflection failures at serde time must fail loudly and not allow a scan to proceed with e.g., missing delete files (since that would produce wrong results silently). As long as the cache can report to callers why they might get |
mbutrovich
self-requested a review
August 3, 2026 13:47
mbutrovich
reviewed
Aug 3, 2026
…g file class The previous test resolved `transform()` on a suite-local class. scalac emits nested classes as public, so the invoke succeeded whether or not `makeAccessible` had run and only the `isAccessible` flag was really checked. Build a real `DataFile` instead: its concrete class `GenericDataFile` and the `BaseFile` that declares its accessors are both package-private in every Iceberg version Comet builds against, so invoking `path()` from the suite's package throws `IllegalAccessException` unless the resolved method had access checks suppressed. Modifier assertions guard against the test going vacuous if those classes ever become public.
mbutrovich
approved these changes
Aug 4, 2026
mbutrovich
left a comment
Contributor
There was a problem hiding this comment.
Approved pending CI, thanks @andygrove!
This was referenced Aug 4, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Part of #5199 (item 2: uncached
Class.getMethodthroughout the Iceberg reflection paths).Rationale for this change
Class.getMethodwalks a class's public method list and returns a fresh defensive copy of theMethodon every call. Comet's Iceberg paths resolve the same handful of accessors once per filescan task, and again per partition field and per delete file, so planning a scan over a table with
many files does O(files) reflective lookups that all resolve to the same few methods. Under AQE that
is repeated for every query stage.
IcebergReflection.extractFileLocationis the worst case: it probes forlocation()on every fileand detects older Iceberg by catching
NoSuchMethodException, so on the versions that only havepath()(Iceberg < 1.7, which is what the Spark 3.4 profile builds against) every call constructs anexception with a stack trace.
Lookup cost, measured in-process against the real
org.apache.iceberg.ContentFileinterface(Spark 4.1 / JDK 17, Iceberg 1.11, 200k iterations, best of 5 after warm-up):
getMethod("location")onContentFileextractFileLocation,location()presentextractFileLocation,path()-only versionEnd to end, serializing a Hadoop-catalog table with 8 partitions x 500 files = 4000 file scan tasks
and no delete files (
CometIcebergNativeScan.serializePartitions, best of 5 per run, three JVMs):Roughly 2.2x. The method caching alone accounts for 38.2 ms -> 20.5 ms; memoizing the per-task
field-id mapping takes it the rest of the way.
CometScanRule.validateIcebergFileScanTasksis unchanged at 3.6-3.7 ms for the same 4000 tasks: itslookups were already hoisted out of the loop, and only the
transform()probe was per task.What changes are included in this PR?
IcebergReflectiongains a resolved-method cache and the lookups now go through it:findMethod/getMethod/findAccessibleMethod/getAccessibleMethod/getDeclaredMethod,plus the existing
findMethodInHierarchy, all read through the cache. Absent methods are cached asmisses, which is what removes the per-file exception on
path()-only Iceberg versions.ClassValuekeyed on the class object, so entries are reclaimed with the class anda cached Iceberg method never pins a classloader Spark has discarded. Overloads are keyed by
parameter type, and
setAccessibleruns once, when a method is first resolved.getFileFormatgains an overload taking an already-loadedContentFileclass, resolving theTODOthat was there;CometScanRuleuses it in the delete-file loop.In
CometIcebergNativeScan.serializePartitionsand its per-task helpers:DeleteFileandPartitionSpecParser/PartitionSpecare loaded once per pass instead of per task(the
PartitionSpecParser.toJsonaccessor is resolved lazily and passed by name, so a failure toresolve it still surfaces as the per-task warning it did before, not an eager failure of the scan).
buildFieldIdMappingis memoized by schema, and the loop-invariant "does the scanschema reference field ids the table schema no longer has" check is hoisted to a lazy val.
Behavior is unchanged throughout:
getMethodstill throwsNoSuchMethodExceptionso the existingcatch blocks keep driving version fallbacks, and the cached lookups return the same methods.
How are these changes tested?
Existing coverage:
CometIcebergNativeSuite(97 tests),CometFuzzIcebergSuite(10),CometIcebergRewriteActionSuite(5) andCometIcebergEncryptionSuite(4) all pass.IcebergReflectionSuitegains unit tests for the cache: that a resolved method is returned byidentity on repeat lookups, that a missing method is cached as a miss and that
getMethodstillthrows
NoSuchMethodExceptionfor one, that overloads are distinguished by parameter type, thatfindMethodInHierarchystill finds an inherited method, and thatextractFileLocationreadslocation()when present, falls back topath()when not (repeatedly, so the cached miss isexercised), and returns None when neither exists.
The probe used for the numbers above was throwaway and is not included.