jamesfredley opened a new issue, #15374:
URL: https://github.com/apache/grails-core/issues/15374
   ### Issue description
   
   Grails 7 exhibits a **~4x performance regression** compared to Grails 6 in 
GORM-based operations when compiled with Invoke Dynamic (Indy), compiling with 
indy off resolves this currently.  
   
   
   ### Root Cause
   
   Grails frameworks frequently modify metaclasses during request processing, 
GORM operations, and testing - triggering mass invalidation cascades that 
prevent JIT optimization.
   
   ---
   
   ## Problem Analysis
   
   ### How Groovy 4 Indy Works
   
   1. **Bootstrap**: When a Groovy method is first called, 
`IndyInterface.bootstrap()` creates a `CacheableCallSite`
   2. **Cache Lookup**: `fromCache()` checks if the method handle is already 
cached for the receiver's class
   3. **Fallback**: If not cached, `selectMethod()` resolves the method and 
caches it
   4. **Optimization**: After `INDY_OPTIMIZE_THRESHOLD` (10,000) hits, the call 
site target is updated to the cached method handle
   
   
   **Problem**: When Grails modifies ANY metaclass (e.g., adding a method to a 
domain class), this invalidates ALL cached call sites in the entire 
application, forcing re-resolution.
   
   ## Recommended Improvements for Grails Framework
   
   ### 1. Framework-Level: Reduce Metaclass Modifications
   
   **Goal**: Minimize the frequency of metaclass changes during request 
processing.
   
   #### 1.1 Lazy/Deferred Metaclass Registration
   
   Instead of modifying metaclasses during request processing, apply all 
metaclass modifications to application startup:
   
   ```groovy
   // grails-core/src/main/groovy/grails/boot/GrailsApp.groovy
   class GrailsApp extends SpringApplication {
       
       @Override
       protected void afterRefresh(ConfigurableApplicationContext context, 
ApplicationArguments args) {
           super.afterRefresh(context, args)
           
           // Trigger all lazy metaclass registrations BEFORE request 
processing begins
           GrailsMetaClassRegistry.instance.finalizeMetaClasses()
       }
   }
   ```
   
   #### 1.2 Batch Metaclass Modifications
   
   When multiple metaclass changes are needed, batch them to trigger only ONE 
invalidation:
   
   ```groovy
   // 
grails-core/src/main/groovy/org/grails/core/metaclass/MetaClassBatcher.groovy
   class MetaClassBatcher {
       private static final ThreadLocal<Boolean> batchMode = new ThreadLocal<>()
       private static final ThreadLocal<List<Runnable>> pendingChanges = new 
ThreadLocal<>()
       
       static void batch(Closure block) {
           batchMode.set(true)
           pendingChanges.set([])
           try {
               block()
               // Apply all changes at once
               pendingChanges.get().each { it.run() }
           } finally {
               batchMode.set(false)
               pendingChanges.remove()
           }
           // Single invalidation for all batched changes
       }
       
       static void scheduleMetaClassChange(Runnable change) {
           if (batchMode.get()) {
               pendingChanges.get().add(change)
           } else {
               change.run()
           }
       }
   }
   ```
   
   ### 2. GORM-Level: Optimize Dynamic Finders
   
   **Goal**: Reduce metaclass modifications from GORM dynamic methods.
   
   #### 2.1 Pre-Register Common Dynamic Finders at Startup
   
   ```groovy
   // 
grails-datastore-gorm/src/main/groovy/org/grails/datastore/gorm/GormEnhancer.groovy
   class GormEnhancer {
       
       void enhanceAll() {
           // Register ALL potential dynamic finders at startup
           domainClasses.each { domainClass ->
               domainClass.persistentProperties.each { property ->
                   preRegisterDynamicFinder(domainClass, property)
               }
           }
       }
       
       private void preRegisterDynamicFinder(Class domainClass, 
PersistentProperty property) {
           def metaClass = domainClass.metaClass
           def propertyName = property.capitilizedName
           
           // Pre-register findBy, findAllBy, countBy, etc.
           ['findBy', 'findAllBy', 'countBy', 'listOrderBy'].each { prefix ->
               def methodName = "${prefix}${propertyName}"
               if (!metaClass.respondsTo(domainClass, methodName)) {
                   metaClass."${methodName}" = { Object[] args ->
                       // Delegate to GORM
                   }
               }
           }
       }
   }
   ```
   
   #### 2.2 Use Method Missing Cache
   
   Instead of adding methods to metaclass dynamically, cache method resolutions:
   
   ```groovy
   // Implement per-class method resolution cache that doesn't modify metaclass
   class DynamicMethodCache {
       private static final Map<String, Closure> methodCache = new 
ConcurrentHashMap<>()
       
       static Closure getMethod(Class clazz, String methodName) {
           String key = "${clazz.name}#${methodName}"
           methodCache.computeIfAbsent(key) { 
               resolveDynamicMethod(clazz, methodName) 
           }
       }
   }
   ```
   
   ### 3. Code Generation: AST Transformations
   
   **Goal**: Generate static method handles at compile time instead of runtime 
metaclass modifications.
   
   #### 3.1 @StaticDynamicFinders AST Transform
   
   ```groovy
   @Target(ElementType.TYPE)
   @Retention(RetentionPolicy.SOURCE)
   
@GroovyASTTransformationClass("org.grails.compiler.StaticDynamicFindersTransformation")
   @interface StaticDynamicFinders {
   }
   
   // Generates static finder methods at compile time
   @StaticDynamicFinders
   class Book {
       String title
       String author
       
       // AST generates:
       // static Book findByTitle(String title) { ... }
       // static Book findByAuthor(String author) { ... }
       // static List<Book> findAllByTitleLike(String titlePattern) { ... }
   }
   ```
   
   #### 3.2 @PrecompiledGormMethods
   
   ```groovy
   // 
grails-compiler/src/main/groovy/org/grails/compiler/PrecompiledGormMethodsTransformation.groovy
   class PrecompiledGormMethodsTransformation extends AbstractASTTransformation 
{
       
       void visit(ASTNode[] nodes, SourceUnit source) {
           ClassNode classNode = nodes[1]
           
           if (isDomainClass(classNode)) {
               // Generate all GORM methods as static compiled methods
               generateSaveMethod(classNode)
               generateDeleteMethod(classNode)
               generateGetMethod(classNode)
               generateListMethod(classNode)
               generateCountMethod(classNode)
               // etc.
           }
       }
   }
   ```
   
   ### 4. Configuration Options for End Users
   
   
   #### 4.1 @CompileStatic for Hot Paths
   
   Identify and annotate performance-critical code:
   
   ```groovy
   // Services that handle many requests
   @CompileStatic
   class BookService {
       
       Book findByIsbn(String isbn) {
           Book.findByIsbn(isbn)  // Compiles to static dispatch
       }
       
       List<Book> search(String query) {
           // Static compilation avoids invokedynamic overhead
       }
   }
   ```
   


-- 
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