Pewarta Proxy

📋 Server Request & Proxy Logs

Loading active proxy settings...
Developed by Khairul Adha (adhexa.id)
Mengambil log server...

⚙️ Pengaturan Proxy Target

Skip to content

Reuse collected sub fields for objects of the same type - #4501

Open
Meemaw wants to merge 2 commits into
graphql-java:masterfrom
Meemaw:perf/reuse-collected-sub-fields
Open

Meemaw wants to merge 2 commits into
graphql-java:masterfrom
Meemaw:perf/reuse-collected-sub-fields

Conversation

@Meemaw

@Meemaw Meemaw commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Problem

ExecutionStrategy.completeValueForObject runs FieldCollector.collectFields for every object in the result. For a list of N objects this repeats the same work N times: walking the selection set, evaluating @skip/@include, expanding fragments and building a fresh LinkedHashMap + MergedFields + MergedSelectionSet.

The result only depends on the merged field, the resolved object type, the variables and the fragments, and the last two are fixed for an execution. Async-profiler on ExecutionBenchmark showed field collection at ~16% of CPU and ~20% of allocated bytes.

Change

The collected sub fields are memoized per execution in ExecutionContext, keyed by MergedField and then by the resolved GraphQLObjectType. Every object of the same type under the same field reuses the same immutable MergedSelectionSet. Sub fields are shared too, so cache hits cascade down the tree.

  • Key correctness: MergedField.equals compares the class and the AST Field nodes (Field uses identity), so two merged fields are only equal when they come from exactly the same set of AST fields. Interface/union results are keyed per concrete object type.
  • Fallback to the old per-object collection when:
    • incremental support (@defer) is enabled, because DeferredExecution instances are created per collection, or
    • a custom ConditionalNodeDecision is in the GraphQLContext, because the callback is not guaranteed to be deterministic.
  • Memory: the cache is bounded by the number of distinct (field, object type) pairs in the operation and lives only as long as the ExecutionContext.
  • API surface: this adds a field to ExecutionContext plus one @Internal getter, getCollectedSubFields(). Happy to move it elsewhere if you prefer.

Benchmarks

Apple Silicon, JDK 21. Base vs this branch jars were run interleaved (alternating, 3 rounds × 2 forks each = 6 forks per side) to cancel out machine drift. Δ shows the 95% confidence interval.

Benchmark Throughput Δ Allocated bytes/op
ExecutionBenchmark.benchmarkThroughput +14.7% ± 7.0% 1,073,041 → 903,689 (−15.8%)
SimpleQueryBenchmark.benchMarkSimpleQueriesThroughput +10.2% ± 9.8% 1,933,273 → 1,510,351 (−21.9%)
IntrospectionBenchmark (large-schema-2) +19.4% ± 9.0% 112.3M → 98.2M (−12.6%)¹

An earlier run on a quieter machine of the same change (before a cosmetic computeIfAbsent refactor) gave tighter intervals: ExecutionBenchmark +9.3% ± 0.8%, SimpleQuery +15.0% ± 1.1%, Introspection +18.4% ± 8.1%. An A/A run (base vs base) on that machine showed no significant difference, with a noise floor of about ±1.8% on ExecutionBenchmark.

¹ Introspection bytes/op vary between forks even for the same jar (96.9M vs 114.5M observed for base), so treat that row as indicative only.

Tests

New CollectedSubFieldsReuseTest covers:

  • objects of different concrete types in the same interface list each get their own type's fields
  • aliased fields with different sub selections are collected separately
  • fields merged across fragments keep all their sub selections
  • collection happens once per (field, object type)
  • a non-deterministic ConditionalNodeDecision is still asked for every object, so the cache is bypassed
  • the cache is bypassed when incremental support is enabled

To check the tests guard the fallbacks, I temporarily removed the bypass condition: the ConditionalNodeDecision test then returned wrong data and the incremental test failed. The full test suite passes, apart from the 14 locale-dependent StringValueParsingUnicodeTest failures that also occur on master with a non-English JVM locale (fixed separately in #4500).

🤖 Generated with Claude Code

completeValueForObject collected the sub fields of the current field
for every object in the result, so a list of N objects repeated the
same selection set walk, @skip/@include evaluation and fragment
expansion N times. The result only depends on the merged field and
the object type, since variables and fragments are fixed for an
execution, so it is now memoized per execution in ExecutionContext.

The cache is bypassed when incremental support is enabled (deferred
executions are created per collection) or when a custom
ConditionalNodeDecision is present (it may not be deterministic).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Meemaw
Meemaw marked this pull request as ready for review October 18, 2026 17:45
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Test Report

Test Results

Java Version Total Passed Failed Errors Skipped
Java 11 - - - - -
Java 17 5968 (-85 🔴) 5911 (-85 🔴) 0 (±0) 0 (±0) 57 (±0)
Java 21 - - - - -
Java 25 5968 (-85 🔴) 5911 (-85 🔴) 0 (±0) 0 (±0) 57 (±0)
jcstress 32 (±0) 32 (±0) 0 (±0) 0 (±0) 0 (±0)
Total 11968 (-170 🔴) 11854 (-170 🔴) 0 (±0) 0 (±0) 114 (±0)

Code Coverage (Java 25)

Metric Covered Missed Coverage vs Master
Lines 30151 3117 90.6% -0.1% 🔴
Branches 8846 1520 85.3% -0.1% 🔴
Methods 8053 1206 87.0% -0.2% 🔴

Changed Class Coverage (25 classes)

Class Line Branch Method
g.a.FieldComplexityEnvironment -7.7% 🔴 ±0.0% -14.3% 🔴
g.e.ExecutionStrategy ±0.0% +0.5% 🟢 +0.1% 🟢
g.e.i.Instrumentation +3.8% 🟢 ±0.0% +4.5% 🟢
g.l.AstPrinter ±0.0% +0.4% 🟢 ±0.0%
g.l.FloatValue
$Builder
-7.1% 🔴 ±0.0% -9.1% 🔴
g.p.UnicodeUtil -4.7% 🔴 ±0.0% ±0.0%
g.r.SimpleListConnection +0.4% 🟢 +0.3% 🟢 ±0.0%
g.s.GraphQLAppliedDirective -5.9% 🔴 ±0.0% -12.5% 🔴
g.s.GraphQLAppliedDirectiveArgument -6.1% 🔴 ±0.0% -10.5% 🔴
g.s.GraphQLArgument -1.6% 🔴 ±0.0% -3.1% 🔴
g.s.GraphQLEnumType -1.2% 🔴 ±0.0% -2.7% 🔴
g.s.GraphQLEnumValueDefinition -2.4% 🔴 ±0.0% -4.2% 🔴
g.s.GraphQLInputObjectField -1.8% 🔴 ±0.0% -3.4% 🔴
g.s.GraphQLInputObjectType -1.8% 🔴 ±0.0% -3.1% 🔴
g.s.GraphQLInterfaceType -1.6% 🔴 ±0.0% -3.4% 🔴
g.s.GraphQLScalarType -9.3% 🔴 ±0.0% -12.5% 🔴
g.s.GraphQLUnionType -1.8% 🔴 ±0.0% -3.8% 🔴
g.s.i.SchemaGeneratorAppliedDirectiveHelper ±0.0% -6.9% 🔴 ±0.0%
g.s.i.SchemaGeneratorHelper ±0.0% -0.7% 🔴 ±0.0%
g.s.i.SchemaPrinter ±0.0% -0.4% 🔴 ±0.0%
g.s.i.TypeDefinitionRegistry -0.6% 🔴 ±0.0% -2.5% 🔴
g.s.v.AppliedDirectiveArgumentsAreValid -5.1% 🔴 -4.5% 🔴 ±0.0%
g.s.v.TypesImplementInterfaces -0.9% 🔴 -5.8% 🔴 ±0.0%
g.u.Anonymizer
$1
-4.8% 🔴 -6.3% 🔴 -3.3% 🔴
g.u.Anonymizer
$2
-3.3% 🔴 -2.8% 🔴 ±0.0%
FieldComplexityEnvironment — method details
Method Line Branch
getArguments 0.0% (-100.0% 🔴) —
FloatValue.Builder — method details
Method Line Branch
ignoredChars 0.0% (-100.0% 🔴) —
UnicodeUtil — method details
Method Line Branch
parseAndWriteUnicode 92.3% (-7.7% 🔴) 100.0%
GraphQLAppliedDirective — method details
Method Line Branch
getDefinition 0.0% (-100.0% 🔴) —
copy 0.0% (-100.0% 🔴) —
GraphQLAppliedDirectiveArgument — method details
Method Line Branch
getDefinition 0.0% (-100.0% 🔴) —
copy 0.0% (-100.0% 🔴) —
GraphQLArgument — method details
Method Line Branch
copy 0.0% (-100.0% 🔴) —
GraphQLEnumType — method details
Method Line Branch
copy 0.0% (-100.0% 🔴) —
GraphQLEnumValueDefinition — method details
Method Line Branch
copy 0.0% (-100.0% 🔴) —
GraphQLInputObjectField — method details
Method Line Branch
copy 0.0% (-100.0% 🔴) —
GraphQLInputObjectType — method details
Method Line Branch
copy 0.0% (-100.0% 🔴) —
GraphQLInterfaceType — method details
Method Line Branch
copy 0.0% (-100.0% 🔴) —
GraphQLScalarType — method details
Method Line Branch
copy 0.0% (-100.0% 🔴) —
withNewChildren 0.0% (-100.0% 🔴) —
lambda$withNewChildren$0 0.0% (-100.0% 🔴) —
GraphQLUnionType — method details
Method Line Branch
copy 0.0% (-100.0% 🔴) —
SchemaGeneratorAppliedDirectiveHelper — method details
Method Line Branch
buildAppliedDirective 100.0% 66.7% (-33.3% 🔴)
buildDirectiveArg 100.0% 66.7% (-16.7% 🔴)
buildAppliedArg 80.0% 33.3% (-16.7% 🔴)
SchemaGeneratorHelper — method details
Method Line Branch
lambda$buildScalar$0 100.0% 50.0% (-50.0% 🔴)
SchemaPrinter — method details
Method Line Branch
lambda$scalarPrinter$1 100.0% 50.0% (-50.0% 🔴)
TypeDefinitionRegistry — method details
Method Line Branch
getType 0.0% (-100.0% 🔴) —
getType 0.0% (-100.0% 🔴) —
AppliedDirectiveArgumentsAreValid — method details
Method Line Branch
checkArgument 93.3% 85.7% (-7.1% 🔴)
isValidExternalValue 50.0% (-50.0% 🔴) —
TypesImplementInterfaces — method details
Method Line Branch
lambda$checkFieldArgumentEquivalence$1 95.7% (-4.3% 🔴) 83.3% (-16.7% 🔴)
Anonymizer.1 — method details
Method Line Branch
visitGraphQLAppliedDirective 92.3% (-7.7% 🔴) 75.0% (-25.0% 🔴)
visitGraphQLScalarType 40.0% (-60.0% 🔴) 50.0% (-50.0% 🔴)
lambda$visitGraphQLScalarType$0 0.0% (-100.0% 🔴) —
Anonymizer.2 — method details
Method Line Branch
visitGraphQLScalarType 40.0% (-60.0% 🔴) 50.0% (-50.0% 🔴)

Full HTML report: build artifact jacoco-html-report

Updated: 2026-10-30 20:28:46 UTC

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants