jdaugherty commented on PR #15800:
URL: https://github.com/apache/grails-core/pull/15800#issuecomment-4905757703

   From using fabled to review this: 
   
   Correctness / performance problems
   
   1. The distinct-factory path is now quadratic — and the PR's own control 
benchmark proves it regressed. removeStaleMetaMethodRegistrationKeys() 
(CodecMetaClassSupport.groovy:224-231) runs keySet().removeIf { key.stale } — a 
full scan of every registered key — under the global lock on every single 
registration. With N distinct factories that's O(N²), which is exactly why the 
PR's control case went from ~9.96s to ~14.4s (+45%). The removeIf sweep also 
makes the ReferenceQueue completely redundant — it's two stale-cleanup 
mechanisms where the queue alone (O(1) amortized) would do.
   
   2. synchronized (emc) locks globally shared metaclasses 
(CodecMetaClassSupport.groovy:178). In production these are the metaclasses of 
String, Object, StringBuilder, etc. Groovy runtime internals and arbitrary user 
code can synchronize on those same monitors; taking them and then nesting the 
global REGISTERED_META_METHODS lock inside creates a contention and 
lock-ordering hazard on some of the hottest objects in the JVM. All codec 
registration also serializes through the one static map lock.
   
   3. The registration key uses the class name, not the class (registrationKey, 
line 235). Under dev reload / plugin classloaders, a reloaded class with the 
same name collides with the old entry, so its registration is skipped and only 
the getMetaMethod fallback saves it — and that fallback is unreliable (next 
point).
   
   4. The fallback check is name-only and cross-factory. add(key) || 
emc.getMetaMethod(methodName, EMPTY_ARGS) == null (line 219): after a metaclass 
is replaced, factory A's re-registration is skipped if any factory has already 
attached a same-named method to the new EMC — so "distinct factories with the 
same codec name stay distinct" (the PR's own claim) breaks across metaclass 
replacement, silently changing which encoder wins. Worse, with 
ExpandoMetaClass.enableGlobally() a freshly created String EMC can inherit the 
expando method from Object's EMC, making getMetaMethod non-null and suppressing 
the re-attach entirely.
   
   5. Your original review concern stands: this is ~150 lines of hand-rolled 
weak-identity caching — custom WeakReference key with identity equals/hashCode, 
a ReferenceQueue, manual stale sweeps, hand-written equals/hashCode on two key 
classes, double-checked locking. Caffeine.newBuilder().weakKeys() gives exactly 
weak identity keys, thread-safe, with none of this — and Caffeine is already 
used in grails-web-url-mappings, grails-datastore-core, grails-rest-transforms, 
etc. All of that machinery exists to dedupe what the PR itself says is ~110 
writes per startup.
   
   6. Is this even the right layer? DefaultCodecLookup.reInitialize() → 
GrailsCodecClass.configureCodecMethods() is where the duplicates originate. 
DefaultGrailsCodecClass already tracks an initialized flag for exactly this 
"called more than once" situation (DefaultGrailsCodecClass.java:324-330). A 
per-codec-class "already registered cached methods" guard there would be a few 
lines with no global static registry at all. The PR never identifies where the 
same-factory duplicate calls come from in a real app, which is the question 
that determines the simplest fix.
   
   Evidence problems
   
   7. The headline numbers aren't reproducible from the branch. The 
"realAppCodecBenchmark" (5 app starts, 31–41% startup improvement) is not in 
this diff, and no baseline write count is given — "110 writes on the branch" is 
meaningless without knowing the baseline count (if baseline was ~130, the dedup 
saved ~20 writes and can't explain a 5-second startup change). A mean of 16.36s 
vs median of 10.03s across 5 samples means one or two massive outliers 
dominated the mean; that's noise, not a measurement. The PR description also 
still references a :grails-encoder:codecMetaClassBenchmark JavaExec task that 
was removed in the third commit.
   
   Test / process problems
   
   8. Tests violate this repo's own rules (CLAUDE.md rule 9) and the production 
class is polluted with test instrumentation. META_METHOD_REGISTRATION_COUNT is 
incremented on every registration forever in production solely so tests can 
read it; clearMetaMethodRegistrationState() and 
getMetaMethodRegistrationKeyCount() exist only for tests. The spec then goes 
further and reflects into the private REGISTERED_META_METHODS field with 
setAccessible(true) (CodecMetaClassSupportSpec.groovy:211-225) to forcibly 
clear weak references. Tests that can only assert via private internals are a 
signal the design isn't observable through its public behavior.
   
   9. Global-state test hygiene. The spec clears process-wide static 
registration state in setup/cleanup, and DefaultGrailsCodecClassTests now 
removes the metaclasses of String, GStringImpl, StringBuffer, StringBuilder, 
and Object in tearDown — global mutations in a suite the project explicitly 
warns runs in parallel. TestLens already flagged a flaky test on this PR's CI 
run.
   
   10. Minor: grails-encoder/build.gradle reads System.properties at 
configuration time for every Test task (configuration-cache unfriendly); the 
benchmark spec carries a main() method and println reporting inside a Spock 
spec; hand-rolled equals/hashCode instead of @EqualsAndHashCode.
   
   Bottom line
   
   The problem is real, but I'd push back on the shape of the fix: (a) identify 
where the duplicate same-factory configureCodecMethods calls actually come from 
and dedupe at the caller (DefaultGrailsCodecClass/DefaultCodecLookup) if 
possible; (b) if a registry is genuinely needed, use Caffeine weakKeys() keyed 
on the factory with the target Class (not its name) in the value key, and drop 
the synchronized(emc) / global-lock / manual sweep machinery; (c) remove the 
production-side counters and make tests observe public behavior; and (d) either 
include the real-app benchmark harness in the PR or restate the impact with the 
baseline write count and a defensible measurement.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to