[ 
https://issues.apache.org/jira/browse/GROOVY-10307?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18056538#comment-18056538
 ] 

ASF GitHub Bot commented on GROOVY-10307:
-----------------------------------------

Copilot commented on code in PR #2377:
URL: https://github.com/apache/groovy/pull/2377#discussion_r2766032370


##########
src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java:
##########
@@ -123,6 +123,18 @@ public void resetFallbackCount() {
         fallbackRound.incrementAndGet();
     }
 
+    /**
+     * Clear the LRU cache and reset fallback count.
+     * Called when metaclass changes to ensure stale method handles are 
discarded.
+     */
+    public void clearCache() {
+        synchronized (lruCache) {
+            lruCache.clear();
+        }
+        latestHitMethodHandleWrapperSoftReference = null;

Review Comment:
   The `clearCache()` method has a potential race condition with the 
`latestHitMethodHandleWrapperSoftReference` field. The field is declared as 
`volatile` (line 48) and is accessed outside the synchronized block (line 134). 
This creates a race condition where:
   
   1. Thread A could be in `getAndPut()` reading 
`latestHitMethodHandleWrapperSoftReference` at line 84
   2. Thread B calls `clearCache()` and sets 
`latestHitMethodHandleWrapperSoftReference = null` at line 134
   3. Thread A proceeds with stale data and might write back the reference at 
line 92
   
   The assignment to `latestHitMethodHandleWrapperSoftReference` should be done 
inside the synchronized block to ensure atomicity with the cache clearing 
operation.
   ```suggestion
               latestHitMethodHandleWrapperSoftReference = null;
           }
   ```



##########
src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java:
##########
@@ -181,24 +189,54 @@ public int getOrderNumber() {
     }
 
     protected static SwitchPoint switchPoint = new SwitchPoint();
+    
+    /**
+     * Weak set of all CacheableCallSites. Used to invalidate caches when 
metaclass changes.
+     * Uses WeakReferences so call sites can be garbage collected when no 
longer referenced.
+     */
+    private static final Set<WeakReference<CacheableCallSite>> ALL_CALL_SITES 
= ConcurrentHashMap.newKeySet(INDY_CALLSITE_INITIAL_CAPACITY);
 
     static {
         
GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu
 -> invalidateSwitchPoints());
     }
+    
+    /**
+     * Register a call site for cache invalidation when metaclass changes.
+     */
+    static void registerCallSite(CacheableCallSite callSite) {
+        ALL_CALL_SITES.add(new WeakReference<>(callSite));

Review Comment:
   The WeakReference cleanup relies entirely on `invalidateSwitchPoints()` 
being called to remove garbage-collected references. If metaclass changes are 
infrequent or never occur in a long-running application, the `ALL_CALL_SITES` 
set could accumulate a large number of cleared WeakReferences (where `ref.get() 
== null`), consuming memory unnecessarily.
   
   Consider adding a periodic cleanup mechanism or implementing cleanup on 
registration if the set size exceeds a threshold. For example, you could check 
the set size in `registerCallSite()` and trigger cleanup if it exceeds a 
configurable threshold, similar to how other Groovy components handle weak 
reference cleanup.
   ```suggestion
   
       /**
        * Threshold for triggering cleanup of cleared WeakReferences from 
ALL_CALL_SITES.
        * Configurable via system property 
"groovy.indy.callsites.cleanup.threshold".
        */
       private static final long ALL_CALL_SITES_CLEANUP_THRESHOLD =
               
SystemUtil.getLongSafe("groovy.indy.callsites.cleanup.threshold", 10_000L);
   
       static {
           
GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu
 -> invalidateSwitchPoints());
       }
       
       /**
        * Remove entries from ALL_CALL_SITES whose referents have been 
garbage-collected.
        */
       private static void cleanupClearedCallSites() {
           ALL_CALL_SITES.removeIf(ref -> ref.get() == null);
       }
       
       /**
        * Register a call site for cache invalidation when metaclass changes.
        */
       static void registerCallSite(CacheableCallSite callSite) {
           ALL_CALL_SITES.add(new WeakReference<>(callSite));
           // Periodically clean up cleared WeakReferences to avoid unbounded 
growth
           if (ALL_CALL_SITES.size() > ALL_CALL_SITES_CLEANUP_THRESHOLD) {
               cleanupClearedCallSites();
           }
   ```



##########
src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java:
##########
@@ -181,24 +189,54 @@ public int getOrderNumber() {
     }
 
     protected static SwitchPoint switchPoint = new SwitchPoint();
+    
+    /**
+     * Weak set of all CacheableCallSites. Used to invalidate caches when 
metaclass changes.
+     * Uses WeakReferences so call sites can be garbage collected when no 
longer referenced.
+     */
+    private static final Set<WeakReference<CacheableCallSite>> ALL_CALL_SITES 
= ConcurrentHashMap.newKeySet(INDY_CALLSITE_INITIAL_CAPACITY);
 
     static {
         
GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu
 -> invalidateSwitchPoints());
     }
+    
+    /**
+     * Register a call site for cache invalidation when metaclass changes.
+     */
+    static void registerCallSite(CacheableCallSite callSite) {
+        ALL_CALL_SITES.add(new WeakReference<>(callSite));
+    }
 
     /**
-     * Callback for constant metaclass update change
+     * Callback for constant metaclass update change.
+     * Invalidates all call site caches to ensure metaclass changes are 
visible.
      */
     protected static void invalidateSwitchPoints() {
         if (LOG_ENABLED) {
-            LOG.info("invalidating switch point");
+            LOG.info("invalidating switch point and call site caches");
         }
 
         synchronized (IndyInterface.class) {
             SwitchPoint old = switchPoint;
             switchPoint = new SwitchPoint();
             SwitchPoint.invalidateAll(new SwitchPoint[]{old});
         }
+        
+        // Invalidate all call site caches and reset targets to default (cache 
lookup).
+        ALL_CALL_SITES.removeIf(ref -> {
+            CacheableCallSite cs = ref.get();
+            if (cs == null) {
+                return true; // Remove garbage collected references
+            }
+            // Reset target to default (fromCache) so next call goes through 
cache lookup
+            MethodHandle defaultTarget = cs.getDefaultTarget();
+            if (defaultTarget != null && cs.getTarget() != defaultTarget) {
+                cs.setTarget(defaultTarget);

Review Comment:
   The `invalidateSwitchPoints()` method has a potential race condition when 
resetting call site targets. The method reads the defaultTarget and 
currentTarget, then conditionally sets the target, all without synchronization 
on the CacheableCallSite object. This creates a race where:
   
   1. Thread A could be calling `setTarget()` on the call site (e.g., during 
optimization)
   2. Thread B (invalidation) reads `cs.getTarget()` and 
`cs.getDefaultTarget()` 
   3. Thread A completes its `setTarget()` call
   4. Thread B then calls `cs.setTarget(defaultTarget)`, potentially 
overwriting Thread A's update
   
   Since `MutableCallSite.setTarget()` is synchronized on the call site itself, 
consider synchronizing on the call site object when checking and resetting the 
target to ensure atomicity.
   ```suggestion
               synchronized (cs) {
                   MethodHandle defaultTarget = cs.getDefaultTarget();
                   MethodHandle currentTarget = cs.getTarget();
                   if (defaultTarget != null && currentTarget != defaultTarget) 
{
                       cs.setTarget(defaultTarget);
                   }
   ```





> Groovy 4 runtime performance on average 2.4x slower than Groovy 3
> -----------------------------------------------------------------
>
>                 Key: GROOVY-10307
>                 URL: https://issues.apache.org/jira/browse/GROOVY-10307
>             Project: Groovy
>          Issue Type: Bug
>          Components: bytecode, performance
>    Affects Versions: 4.0.0-beta-1, 3.0.9
>         Environment: OpenJDK Runtime Environment AdoptOpenJDK-11.0.11+9 
> (build 11.0.11+9)
> OpenJDK 64-Bit Server VM AdoptOpenJDK-11.0.11+9 (build 11.0.11+9, mixed mode)
> WIN10 (tests) / REL 8 (web application)
> IntelliJ 2021.2 
>            Reporter: mgroovy
>            Priority: Major
>         Attachments: groovy_3_0_9_gc.png, groovy_3_0_9_loop2.png, 
> groovy_3_0_9_loop4.png, groovy_3_0_9_mem.png, groovy_4_0_0_b1_loop2.png, 
> groovy_4_0_0_b1_loop4.png, groovy_4_0_0_b1_loop4_gc.png, 
> groovy_4_0_0_b1_loop4_mem.png, 
> groovysql_performance_groovy4_2_xx_yy_zzzz.groovy, loops.groovy, 
> profile3.txt, profile4-loops.txt, profile4.txt, profile4d.txt
>
>
> Groovy 4.0.0-beta-1 runtime performance in our framework is on average 2 to 3 
> times slower compared to using Groovy 3.0.9 (regular i.e. non-INDY)
> * Our complete framework and application code is completely written in 
> Groovy, spread over multiple IntelliJ modules
> ** mixed @CompileDynamic/@TypeChecked and @CompileStatic
> ** No Java classes left in project, i.e. no cross compilation occurs
> * We build using IntelliJ 2021.2 Groovy build process, then run / deploy the 
> compiled class files
> ** We do _not_ use a Groovy based DSL, nor do we execute Groovy scripts 
> during execution
> * Performance degradation when using Groovy 4.0.0-beta-1 instead of Groovy 
> 3.0.9 (non-INDY):
> ** The performance of the largest of our web applications has dropped 3x 
> (startup) / 2x (table refresh) respectively
> *** Stack: Tomcat/Vaadin/Ebean plus framework generated SQL
> ** Our test suite runs about 2.4 times as long as before (120 min when using 
> G4, compared to about 50 min with G3)
> *** JUnit 5 
> *** test suite also contains no scripts / dynamic code execution
> *** Individual test performance varies: A small number of tests runs faster, 
> but the majority is slower, with some extreme cases taking nearly 10x as long 
> to finish
> * Using Groovy 3.0.9 INDY displays nearly identical performance degradation, 
> so it seems that the use of invoke dynamic is somehow at fault



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to