Reduce allocations in Async$Many and ResultPath on execution hot path#4252
Merged
Reduce allocations in Async$Many and ResultPath on execution hot path#4252
Conversation
In the all-synchronous execution path (no CompletableFutures), Async$Many allocated an Object[] to collect field values, then copied them into a new ArrayList in materialisedList(). Replace the copy with Arrays.asList() which wraps the existing array at zero cost. Benchmarked with a new ExecutionBenchmark (balanced tree: ~530 fields, ~2000 result scalars, depth 5) showing ~5% throughput improvement on the synchronous path. Also adds async-profiler support to build.gradle for JMH profiling. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Contributor
Test Results 335 files ±0 335 suites ±0 5m 3s ⏱️ -1s Results for commit c52498b. ± Comparison against base commit f8f9892. This pull request removes 196 and adds 172 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
…cations The toString representation of ResultPath was eagerly computed in the constructor via initString(), but is never read during normal query execution — only used for error reporting. Make it lazy (computed on first toString() call) to eliminate all string work from the hot path. Also inline segmentToString() into initString() to avoid intermediate String allocations when the value is eventually computed, letting Java's StringConcatFactory handle it as a single multi-arg concat. Benchmarked ~30-78% throughput improvement vs master across all execution benchmarks. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Every field fetch created a throwaway FieldCoordinates object just for a HashMap lookup. Add an internal nested Map<String, Map<String, ...>> (typeName → fieldName → factory) built at CodeRegistry construction time, and an internal getDataFetcher(String, String, GraphQLFieldDefinition) method that does the lookup by strings directly. Use this in ExecutionStrategy.fetchField to skip FieldCoordinates creation entirely. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ized-list-optimization
Remove @internal from the String-based getDataFetcher overload and add proper javadoc documenting the ~54 KB/op allocation savings and 5-9% throughput improvement over the FieldCoordinates-based lookup. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…init Since toStringValue is lazily computed (once per path), the manual inlining of segmentToString() provides no measurable performance benefit. Simplify back to the clean delegation. Co-Authored-By: Claude Opus 4.6 <[email protected]>
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.
Summary
Three execution hot-path optimizations identified via async-profiler:
ArrayListcopy with zero-copyArrays.asList()wrapper — eliminates one allocation + array copy per selection set on the synchronous execution pathtoString()call) — the string representation is never read during normal execution, only for error reportinggetDataFetcher(String, String, GraphQLFieldDefinition)overload using a nestedMap<String, Map<String, DataFetcherFactory<?>>>for fast lookup by type/field name strings, avoiding creation of a throwawayFieldCoordinatesobject on every field fetch (~54 KB/op reduction in allocations)Also adds
ExecutionBenchmarkJMH benchmark and async-profiler support tobuild.gradle.Benchmark results
Existing benchmarks (master vs optimized)
ExecutionBenchmark (new — only on optimized branch)
Test plan
./gradlew test)Arrays.asListreturns a fixed-size list — verified no consumer mutates the returned listResultPath.toString()verified safe: same pattern ashashCode()(racy single-check idiom on immutable data)getDataFetcher— same lookup logic, different key structure🤖 Generated with Claude Code