GROOVY-10307: Groovy 6 - Improve invokedynamic performance with optimized caching#2377
GROOVY-10307: Groovy 6 - Improve invokedynamic performance with optimized caching#2377jamesfredley wants to merge 6 commits intoapache:masterfrom
Conversation
Change @InputFiles @classpath to @input on untouchedFiles field. This field contains glob patterns (strings), not actual files, so @InputFiles and @classpath were inappropriate and caused Gradle to treat patterns containing '*' as literal file paths on Windows.
Reduce the performance impact of metaclass changes on invokedynamic call sites: - Disable global SwitchPoint guard by default (controlled via groovy.indy.switchpoint.guard) - Track all call sites via WeakReference set for targeted invalidation - Add clearCache() method to CacheableCallSite for cache invalidation on metaclass change - When metaclass changes, clear caches and reset targets on all registered call sites This provides ~19% improvement on metaclass invalidation stress tests compared to baseline Groovy 6.x, and ~24% improvement compared to Groovy 4.0.30.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2377 +/- ##
==================================================
+ Coverage 66.7519% 66.7548% +0.0029%
- Complexity 29851 29857 +6
==================================================
Files 1382 1382
Lines 116136 116158 +22
Branches 20475 20478 +3
==================================================
+ Hits 77523 77541 +18
- Misses 32278 32283 +5
+ Partials 6335 6334 -1
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This pull request improves invokedynamic performance in Groovy 6 by replacing the global SwitchPoint invalidation mechanism with targeted per-call-site cache invalidation. When metaclasses change in Groovy, instead of invalidating ALL call sites across the application, only the cached method handles at each call site are cleared, significantly reducing overhead in applications that frequently modify metaclasses.
Changes:
- Introduced a call site registry using WeakReferences to track all active call sites for targeted invalidation
- Made the global SwitchPoint guard optional (disabled by default) via the
groovy.indy.switchpoint.guardsystem property - Added cache clearing mechanism to CacheableCallSite for metaclass change handling
- Fixed a Windows build issue in JarJarTask by correcting the annotation for glob pattern strings
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java | Made global SwitchPoint guard conditional via INDY_SWITCHPOINT_GUARD property (disabled by default) |
| src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java | Added WeakReference-based call site registry and targeted cache invalidation logic |
| src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java | Added clearCache() method to clear LRU cache and reset fallback count on metaclass changes |
| build-logic/src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy | Fixed annotation from @InputFiles @classpath to @input for glob pattern strings |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Resolved import conflict in IndyInterface.java by keeping both sets of imports: - Set, ConcurrentHashMap (needed for ALL_CALL_SITES tracking) - Function (existing in master-origin)
Merge master into groovy-10307-indy-optimization
|
Can now be tested with JMH benchmarks added on: #2381 Run with: |
…oints Add 43 JMH benchmarks across 4 files targeting the metaclass invalidation patterns that cause performance regression in Grails applications under invokedynamic. These complement the general-purpose benchmarks from apache#2381 by exercising the specific dynamic dispatch patterns identified in apache#2374 and apache#2377. New benchmark files: MetaclassChangeBench (11 benchmarks) - ExpandoMetaClass method addition during method dispatch - Metaclass replacement cycles - Multi-class invalidation cascade (change on ServiceA invalidates ServiceB/C) - Burst-then-steady-state (simulates Grails startup then request handling) - Property access and closure dispatch under metaclass churn CategoryBench (10 benchmarks) - use(Category) blocks inside vs outside loops - Nested and simultaneous multi-category scopes - Collateral invalidation damage on non-category call sites - Category method shadowing existing methods DynamicDispatchBench (12 benchmarks) - methodMissing with single/rotating names (dynamic finders) - propertyMissing read/write (Grails params/session) - GroovyInterceptable invokeMethod interception (transactional services) - ExpandoMetaClass-injected method calls mixed with real methods - def-typed monomorphic and polymorphic dispatch GrailsLikePatternsBench (10 benchmarks) - Service chain: validation, CRUD, collection processing - Controller action: param binding, service call, model/view rendering - Domain validation with dynamic property access (this."$field") - Configuration DSL with nested @DelegatesTo closures - Markup builder with nested tag/closure rendering - Full request cycle simulation with and without metaclass churn Run with: ./gradlew perf:jmh -PbenchInclude=perf
…oints Add 43 JMH benchmarks across 4 files targeting the metaclass invalidation patterns that cause performance regression in Grails applications under invokedynamic. These complement the general-purpose benchmarks from apache#2381 by exercising the specific dynamic dispatch patterns identified in apache#2374 and apache#2377. New benchmark files: MetaclassChangeBench (11 benchmarks) - ExpandoMetaClass method addition during method dispatch - Metaclass replacement cycles - Multi-class invalidation cascade (change on ServiceA invalidates ServiceB/C) - Burst-then-steady-state (simulates Grails startup then request handling) - Property access and closure dispatch under metaclass churn CategoryBench (10 benchmarks) - use(Category) blocks inside vs outside loops - Nested and simultaneous multi-category scopes - Collateral invalidation damage on non-category call sites - Category method shadowing existing methods DynamicDispatchBench (12 benchmarks) - methodMissing with single/rotating names (dynamic finders) - propertyMissing read/write (Grails params/session) - GroovyInterceptable invokeMethod interception (transactional services) - ExpandoMetaClass-injected method calls mixed with real methods - def-typed monomorphic and polymorphic dispatch GrailsLikePatternsBench (10 benchmarks) - Service chain: validation, CRUD, collection processing - Controller action: param binding, service call, model/view rendering - Domain validation with dynamic property access (this."$field") - Configuration DSL with nested @DelegatesTo closures - Markup builder with nested tag/closure rendering - Full request cycle simulation with and without metaclass churn Run with: ./gradlew perf:jmh -PbenchInclude=perf
…oints Add 43 JMH benchmarks across 4 files targeting the metaclass invalidation patterns that cause performance regression in Grails applications under invokedynamic. These complement the general-purpose benchmarks from #2381 by exercising the specific dynamic dispatch patterns identified in #2374 and #2377. New benchmark files: MetaclassChangeBench (11 benchmarks) - ExpandoMetaClass method addition during method dispatch - Metaclass replacement cycles - Multi-class invalidation cascade (change on ServiceA invalidates ServiceB/C) - Burst-then-steady-state (simulates Grails startup then request handling) - Property access and closure dispatch under metaclass churn CategoryBench (10 benchmarks) - use(Category) blocks inside vs outside loops - Nested and simultaneous multi-category scopes - Collateral invalidation damage on non-category call sites - Category method shadowing existing methods DynamicDispatchBench (12 benchmarks) - methodMissing with single/rotating names (dynamic finders) - propertyMissing read/write (Grails params/session) - GroovyInterceptable invokeMethod interception (transactional services) - ExpandoMetaClass-injected method calls mixed with real methods - def-typed monomorphic and polymorphic dispatch GrailsLikePatternsBench (10 benchmarks) - Service chain: validation, CRUD, collection processing - Controller action: param binding, service call, model/view rendering - Domain validation with dynamic property access (this."$field") - Configuration DSL with nested @DelegatesTo closures - Markup builder with nested tag/closure rendering - Full request cycle simulation with and without metaclass churn Run with: ./gradlew perf:jmh -PbenchInclude=perf
merge master into groovy-10307-indy-optimization
|
After adding addition JMH benchmarks, it is clear these changes do not benefit Groovy 6, like they did on Groovy 4, given the dozens of indy related changes/enhancements between 4 and 6. I am going to close this PR. |
Summary
Based on #2374, but applied to master(Groovy 6) instead of GROOVY_4_0_X
This PR improves invokedynamic performance reported in GROOVY-10307. The optimization reduces the performance impact of metaclass changes on call sites by replacing the global SwitchPoint invalidation mechanism with targeted per-call-site cache invalidation.
Problem
When any metaclass changes in Groovy, the global
SwitchPointis invalidated, causing all invokedynamic call sites across the entire application to fall back and re-link. This creates significant overhead in applications that frequently modify metaclasses (e.g., Grails applications with dynamic finders, runtime mixins, etc.).Solution
This PR implements a more targeted invalidation strategy:
Disable global SwitchPoint guard by default - The
SwitchPoint.guardWithTest()wrapper is now optional and disabled by default. This prevents mass invalidation of all call sites when any metaclass changes.Track all call sites via WeakReference set - All
CacheableCallSiteinstances are registered in a concurrent set using weak references, allowing targeted invalidation without preventing garbage collection.Add
clearCache()method to CacheableCallSite - When a metaclass changes, we can now clear the LRU cache and reset the fallback count on specific call sites rather than invalidating everything.Targeted invalidation on metaclass change -
invalidateSwitchPoints()now iterates through registered call sites, clearing caches and resetting targets as needed.Changes
CacheableCallSite.javaclearCache()method to clear LRU cache and reset fallback countIndyInterface.javaALL_CALL_SITESWeakReference set to track all call sitesregisterCallSite(CacheableCallSite)methodinvalidateSwitchPoints()to clear caches on all registered call sitesSelector.javaINDY_SWITCHPOINT_GUARDsystem property flag (default:false)JarJarTask.groovy(unrelated build fix)@InputFiles @Classpathto@InputonuntouchedFilesfield*were treated as literal file pathsConfiguration
The SwitchPoint guard behavior can be controlled via system property:
Benchmark Results
Tested using a dedicated benchmark suite measuring metaclass invalidation impact: https://github.com/jamesfredley/groovy-indy-performance
Complete Benchmark Comparison (3-Run Averages)
Test Date: February 4, 2026
Test Machine: Windows 11, 20 cores, 4GB max heap
Java Version: 17.0.18 (Amazon Corretto)
Versions Tested
Optimizations Applied in 6-snapshot-opt
INDY_SWITCHPOINT_GUARDdefaults tofalseWeakReferenceset🎯 KEY METRIC: Metaclass Invalidation Stress Test
This test measures the performance impact when metaclass changes occur during execution.
Lower ratio = better (less performance degradation from metaclass changes).
Key Finding
6-snapshot-opt reduces metaclass invalidation impact by:
Comprehensive Benchmark Suite (3-Run Averages)
Closure Benchmark Suite (3-Run Averages)
Loop Benchmark Suite (3-Run Averages)
Method Invocation Benchmark Suite (3-Run Averages)
Raw Data: Individual Run Results
Metaclass Invalidation Ratios
With Metaclass Changes (ms)
Baseline (No Metaclass Changes) (ms)
Existing Test Coverage:
Related