perf(java): skip runtime NON_EMPTY checks for final JSON property types - #4111
pavel-ptashyts wants to merge 1 commit into
Conversation
|
AI review artifact - Fory-guided reviewer (fresh session, prompted to follow |
|
AI review artifact - independent general reviewer (separate clean-context session, not pointed to Reviewed commit: No further actionable comments. For transparency, the things I attacked and what resolved them: Soundness of the predicate. No NPE / no unreachable-type hazard. The reflective lookup. Generated-class sharing. Behavior-preserving rewrite. The Sizing. |
|
AI review artifact - Fory-guided reviewer (fresh session, prompted to follow |
c65a64e to
48dd8d8
Compare
|
AI review artifact - independent general reviewer (separate clean-context session, not pointed to I reviewed What I checked, and why I did not raise it: Soundness of the generation-time decision. The decision is
NPE risk on Key coverage. Beyond the scoped-out verification, I walked the public registration surface: Behaviour deltas. Test. Both polarities of the No further actionable comments. |
Since apache#4074 the generated JSON writer calls JsonFieldInfo.isEmpty with the property's write type info for every omit-empty property whose declared type is not a CharSequence, array, Collection, Map or Optional. When JsonFieldInfo.mayBeEmpty rules out every built-in empty type (a final or enum declared type), the only possible true answer comes from the property's write codec; when that codec keeps the default JsonValueCodec.isEmpty, the value is never empty. The generated writers now decide this while generating, emit only the null check for such properties and leave the check out of the group size estimate. Non-final declared types (including BigDecimal and BigInteger) and codecs that override isEmpty keep the runtime call. The decision stays in code generation: native images resolve types at run time without reflection metadata for codec methods. The registered codec class is already part of the generated class key, so the decision cannot be shared with an instance that registers another codec. Fixes apache#4105 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
48dd8d8 to
384b382
Compare
Why?
Since #4074 the generated JSON writer calls
JsonFieldInfo.isEmpty(value, wpN.writeTypeInfo(), writer)for every omit-empty property whose declared type is not aCharSequence, array,Collection,MaporOptional*. For boxed numbers,Boolean, enums and other final types that call can only ever returnfalse, yet every write pays theinstanceofchain inJsonFieldInfo.isEmptyplus an interface call to the codec. WithNON_EMPTYas the default inclusion this makes writes of number-heavy beans about 2.2 times slower than in 1.7.3 (details and a reproducer in #4105).What does this PR do?
JsonWriterCodegen.neverEmptydecides the emptiness check while the writer is generated: whenJsonFieldInfo.mayBeEmpty()rules out every built-in empty type (a final or enum declared type) and neither the property's write codec class nor any of its superclasses or superinterfaces declaresisEmpty(only theJsonValueCodecdefault applies), the generated String and UTF-8 writers emit only the null check, andfieldWriteSizeno longer counts a check that is not emitted.BigDecimalandBigInteger, which are not final), and codecs that overrideisEmpty, whether they come fromregisterCodec, a codec factory,@JsonCodecor a Mixin.isEmptylookup must not run inJsonFieldInfo.resolveTypes.ForyJsoninstances. The decision depends only on the declared type, fixed by the owner class, and on the write codec class, which the generated class key already carries (EXACT_CODEC,FACTORY,MIXIN);GeneratedCodecKeyBuilder.addRegistrationnow notes that dependency. ThemayBeEmptyandisEmptyjavadocs point at each other, since the built-in empty types must stay in sync.JsonGeneratedCodecTest:nonEmptyFinalValuesinspects the generated String and UTF-8 writer sources (no runtime check forInteger,Long, a plain enum, an enum with constant bodies and a final bean; the check kept for a final type with anisEmpty-overriding@JsonCodec, forBigDecimaland forObject), checks the written JSON, and compares every fixture, including a 31-property bean that moves the generated group boundaries, with the interpreted writer. The wide bean's source is checked too, and it is split into member helpers, so the grouped writer shape is covered. It fails without the codegen change.nonEmptyRegisteredCodecbuilds two instances in one JVM, one with a default-emptiness codec and one with anisEmpty-overriding codec for the same final type, throughregisterCodec, a codec factory, a Mixin, a codec inheritingisEmptyfrom an interface and one inheriting it from an abstract base class. It checks that both instances run different generated UTF-8 writers, that only the overriding one keeps the runtime check, and that it still omits the empty value after the other one generated its writer. It fails if the codec override, or the interface or superclass part of the walk, is ignored.builtInEmptyTypesassertsmayBeEmpty()for every typeJsonFieldInfo.isEmptytests itself, including an enum implementingCharSequence, so the two lists cannot drift.Related issues
AI Contribution Checklist
yesyes, I included a completed AI Contribution Checklist in this PR description and the requiredAI Usage Disclosure.yes, my PR description includes the requiredai_reviewsummary and screenshot evidence or equivalent persisted links of the final clean AI review results from both fresh reviewers described inAI_POLICY.md, the Fory-guided reviewer and the independent general reviewer, on the current PR diff or current HEAD after the latest code changes.Completed checklist from
AI_POLICY.mdsection 9:yesAI Usage Disclosureblock below.AGENTS.mdand.agents/ci-and-pr.md, and one independent general reviewer in a separate clean-context review session that was not pointed to.agents/ci-and-pr.mdor any copied Fory-specific review checklist.Verification
JDK 25.0.2, Windows 11 x86_64, from
java/:mvn -pl fory-json -am install -DskipTests: successmvn -pl fory-json test: 1272 tests, 0 failures, 0 errors, 0 skippedmvn -pl fory-json test -Dtest='JsonGeneratedCodecTest#nonEmptyFinalValues'with theJsonWriterCodegenchange reverted: fails, as intendedJsonGeneratedCodecTest#nonEmptyRegisteredCodecwith the codec-override check disabled: fails, as intendedJsonGeneratedCodecTest#builtInEmptyTypeswithOptionalIntremoved frommayBeEmpty, or with an early enum return inmayBeEmpty: fails, as intendedJsonGeneratedCodecTest#nonEmptyRegisteredCodecwith the interface or the superclass part of the override walk removed: fails, as intendedmvn -pl fory-json spotless:check checkstyle:check: success, 0 Checkstyle violationsDoes this PR introduce any user-facing change?
Benchmark
Standalone harness from #4105 (not JMH): a bean with 10
String, 20Integer, 10Long, 2Double, 2Boolean, 3 enum, 2BigDecimalproperties and oneList<String>, all non-null, serialized withForyJson.toJsonBytesafter a 20 s warm-up; median of 5 rounds of 2,000,000 writes; one JVM per configuration, run one after another on an otherwise idle host; JDK 25.0.2,-Xms1g -Xmx1g. ns per write, two repetitions:NON_EMPTYNON_NULLThe JSON written is identical between the two builds in both modes (compared by digest of the output JSON line). The two
BigDecimalproperties still take the runtime check; the remainingNON_EMPTYcost overNON_NULLis within noise here.🤖 Generated with Claude Code