Reduce cached codec metaclass registration overhead - #15800
Conversation
Assisted-by: Hephaestus:gpt-5.5
Assisted-by: Hephaestus:gpt-5.5
There was a problem hiding this comment.
Pull request overview
This pull request reduces startup/runtime overhead from repeated cached codec registration by making cached encodeAs* / decode* ExpandoMetaClass writes idempotent per (target class, codec method name, codec factory identity), while preserving non-cached (development/reload) behavior and distinct-factory semantics.
Changes:
- Add a weak-identity registration key and per-
ExpandoMetaClasssynchronization to skip duplicate cached meta-method writes. - Add focused Spock coverage for cached idempotence, metaclass replacement, distinct factories, and non-cached re-registration.
- Add a benchmark harness plus a
:grails-encoder:codecMetaClassBenchmarkGradle task to measure registration cost and write counts.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| grails-encoder/src/main/groovy/org/grails/encoder/CodecMetaClassSupport.groovy | Adds cached-registration dedupe bookkeeping and synchronized duplicate checks to avoid repeated ExpandoMetaClass mutation. |
| grails-encoder/src/test/groovy/org/grails/encoder/CodecMetaClassSupportSpec.groovy | Adds targeted tests covering idempotence, concurrency, metaclass replacement, stale-key pruning, and non-cached behavior. |
| grails-encoder/src/test/groovy/org/grails/encoder/CodecMetaClassBenchmark.groovy | Adds a runnable benchmark main for codec registration and encode-path timing/counters. |
| grails-encoder/build.gradle | Registers a JavaExec benchmark task and forwards grails.codec.benchmark.* system properties. |
| grails-test-suite-uber/src/test/groovy/org/grails/commons/DefaultGrailsCodecClassTests.groovy | Adds regression coverage calling configureCodecMethods() twice and validates public dynamic methods still work; improves metaclass cleanup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15800 +/- ##
==================================================
+ Coverage 49.3866% 49.4050% +0.0184%
- Complexity 16810 16828 +18
==================================================
Files 1993 1993
Lines 93495 93531 +36
Branches 16360 16369 +9
==================================================
+ Hits 46174 46209 +35
+ Misses 40189 40186 -3
- Partials 7132 7136 +4
🚀 New features to boost your workflow:
|
jdaugherty
left a comment
There was a problem hiding this comment.
I'm worried about the memory impacts this has on large applications. It's rolling it's own caching mechanism when elsewhere in the code base we've used other solutions like caffeine, etc.
Assisted-by: opencode:gpt-5.5
Assisted-by: opencode:gpt-5.5
|
From using fabled to review this: Review: PR #15800 — Reduce cached codec metaclass registration overheadFirst, confirming one premise: repeated Correctness / performance problems1. The distinct-factory path is now quadratic — and the PR's own control benchmark private static void removeStaleMetaMethodRegistrationKeys() {
CodecFactoryKey codecFactoryKey = (CodecFactoryKey) STALE_CODEC_FACTORIES.poll()
while (codecFactoryKey != null) {
REGISTERED_META_METHODS.remove(codecFactoryKey)
codecFactoryKey = (CodecFactoryKey) STALE_CODEC_FACTORIES.poll()
}
REGISTERED_META_METHODS.keySet().removeIf { CodecFactoryKey key -> key.stale }
}With N distinct factories that's O(N²), which is exactly why the PR's control case went 2. 3. The registration key uses the class name, not the class ( private static MetaMethodRegistrationKey registrationKey(ExpandoMetaClass emc, String methodName) {
new MetaMethodRegistrationKey(emc.getTheClass().getName(), methodName)
}Under dev reload / plugin classloaders, a reloaded class with the same name collides 4. The fallback check is name-only and cross-factory (line 219): registeredMetaMethodKeys(codecFactory).add(key) || emc.getMetaMethod(methodName, EMPTY_ARGS) == nullAfter a metaclass is replaced, factory A's re-registration is skipped if any factory 5. This is ~150 lines of hand-rolled weak-identity caching — custom 6. Is this even the right layer? Evidence problems7. The headline numbers aren't reproducible from the branch. The Test / process problems8. Tests violate the repo's own rules (CLAUDE.md rule 9 — test via public APIs) and private static Set registeredMetaMethodKeys() {
def field = CodecMetaClassSupport.getDeclaredField('REGISTERED_META_METHODS')
field.accessible = true
((Map) field.get(null)).keySet()
}Tests that can only assert via private internals are a signal the design isn't 9. Global-state test hygiene. The spec clears process-wide static registration state 10. Minor. Bottom lineThe problem is real, but I'd push back on the shape of the fix:
|
…class-registration-overhead
…class-registration-overhead
Keep synchronized metaclass mutation, avoid retaining replaced ExpandoMetaClass instances, and restore benchmark test JVM wiring. Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]
✅ All tests passed ✅🏷️ Commit: ac31469 Learn more about TestLens at testlens.app. |
|
The problem this targets is real - repeated
So I agree with the direction you outlined:
I'll rework it this way. Thanks for the thorough review - this one's a genuinely better design after the feedback. |
|
@jamesfredley let me know once all of these changes are pushed and I'll review again. |
The Problem
Fixes #15374.
Grails codec registration dynamically adds
encodeAs*anddecode*methods onto globally hot metaclasses -String,GStringImpl,StringBuffer,StringBuilder, andObject. On Groovy Indy, repeatedly mutating those globally-usedExpandoMetaClasstypes invalidates call sites and creates avoidable startup/runtime overhead. The clearest, fully compatible win is to stop re-adding the exact same cached codec method when the sameCodecFactoryis already registered for the same target metaclass.The Fix
Skip redundant cached metaclass writes while preserving every existing behavior.
CodecMetaClassSupportnow tracks cached registrations by target class + codec method name +CodecFactoryidentity.identityHashCodealone), so distinct factories that expose the same codec name are not collapsed.ExpandoMetaClasswrite; the first still installs the normal closures.ExpandoMetaClassso parallel registration cannot race into duplicate writes.What is deliberately preserved
value.encodeAsHTML(),value.encodeAsURL(),value.decodeSomeCodec()exactly as before.Measured impact
Micro-benchmark (
:grails-encoder:codecMetaClassBenchmark, 10,000 same-factory registrations):8.0.xReal Grails app (
grails-test-examples-app1, opt-inrealAppCodecBenchmark, 5 full app starts):8.0.xBranch counter shows exactly 110 codec metaclass writes per full
app1startup.Scope
This is the codec-registration slice of the broader #15374 investigation. GORM dynamic methods, taglib dispatch, static-compilation opportunities, artefact indexing, and metaclass freeze/warning mechanisms were examined but are not changed here - they carry a larger compatibility/review surface. No user-facing docs change is required (no new setting, workflow, API, or syntax).
Testing
:grails-encoder:test(addsCodecMetaClassSupportSpec: same-factory idempotence, re-registration after metaclass replacement, distinct-factory isolation, non-cached re-registration).:grails-test-examples-gsp-layout:integrationTestGSPencodeAsHTML prevents XSS/encodeAsURL encodes URL parameterspass through a real browser-driven app surface.