-
-
Notifications
You must be signed in to change notification settings - Fork 970
Description
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
- Bootstrap: When a Groovy method is first called,
IndyInterface.bootstrap()creates aCacheableCallSite - Cache Lookup:
fromCache()checks if the method handle is already cached for the receiver's class - Fallback: If not cached,
selectMethod()resolves the method and caches it - 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
Labels
Type
Projects
Status