Skip to content

Grails 8 grails-core: Invoke Dynamic (Indy) Optimization Opportunities #15374

@jamesfredley

Description

@jamesfredley

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:

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

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

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

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

@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

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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions