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]