This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch feat/indy-default-groovy6
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 37750d854fccf62e091e5660c241de10b69fdeff
Author: James Fredley <[email protected]>
AuthorDate: Tue Aug 18 13:20:27 2026 -0400

    feat: enable Groovy invokedynamic by default on Grails 9
    
    Grails 7 turned indy off because Groovy 4 paid a 2-8x penalty (#15293).
    Grails 9 is on Groovy 6, where microbenchmarks were already positive and
    app-level results were flat to slightly negative. Make indy the default
    so generated apps match Groovy 6's preferred dispatch.
    
    Opt out with grails { indy = false }. Non-indy still needs groovy-callsite,
    which the BOM already manages. Framework modules honor -PgrailsIndy so CI
    and JMH can still A/B both modes.
    
    Assisted-by: claude-code:claude-opus-5
---
 .../apache/grails/buildsrc/CompilePlugin.groovy    | 17 +++++
 .../grails/buildsrc/CompilePluginIndySpec.groovy   | 73 ++++++++++++++++++++++
 grails-doc/src/en/guide/introduction/whatsNew.adoc | 18 ++++++
 .../gradle/plugin/core/GrailsExtension.groovy      |  8 +--
 .../gradle/plugin/core/GrailsGradlePlugin.groovy   | 11 ++--
 .../gradle/plugin/core/GrailsExtensionSpec.groovy  | 23 +++++++
 6 files changed, 142 insertions(+), 8 deletions(-)

diff --git 
a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy
 
b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy
index e8375e9f18..8edecbf3a0 100644
--- 
a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy
+++ 
b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy
@@ -102,6 +102,13 @@ class CompilePlugin implements Plugin<Project> {
             }
         }
 
+        // Framework modules do not apply the Grails Gradle plugin, so grails 
{ indy = ... } never
+        // reaches them. Honour the same -PgrailsIndy toggle used by 
grails-extension-gradle-config
+        // (and CI matrix.indy). Property absent => leave Groovy's default 
alone.
+        final Boolean frameworkIndy = project.hasProperty('grailsIndy')
+                ? Boolean.parseBoolean(project.property('grailsIndy') as 
String)
+                : null
+
         project.plugins.withId('groovy') {
             project.tasks.withType(GroovyCompile).configureEach {
                 // encoding needs to be the same since it's different across 
platforms
@@ -133,10 +140,20 @@ class CompilePlugin implements Plugin<Project> {
                 // when both are present.
                 it.groovyOptions.configurationScript =
                         
GradleUtils.findRootGrailsCoreDir(project).file('gradle/groovy-compile-configscript.groovy').asFile
+                if (frameworkIndy != null) {
+                    it.groovyOptions.optimizationOptions.indy = frameworkIndy
+                    it.inputs.property('grailsIndy', frameworkIndy)
+                }
             }
             project.tasks.withType(Test).configureEach {
                 
it.jvmArgs('-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true')
             }
+            // GROOVY-11158: classic (non-indy) call-site bytecode lives in 
groovy-callsite.
+            if (Boolean.FALSE.equals(frameworkIndy)
+                    && project.rootProject.findProject(':grails-bom') != null
+                    && project.configurations.findByName('implementation') != 
null) {
+                project.dependencies.add('implementation', 
'org.apache.groovy:groovy-callsite')
+            }
         }
     }
 
diff --git 
a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginIndySpec.groovy
 
b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginIndySpec.groovy
new file mode 100644
index 0000000000..e8a634d62f
--- /dev/null
+++ 
b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginIndySpec.groovy
@@ -0,0 +1,73 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.buildsrc
+
+import org.gradle.api.Project
+import org.gradle.api.tasks.compile.GroovyCompile
+import org.gradle.testfixtures.ProjectBuilder
+import spock.lang.Specification
+import spock.lang.TempDir
+
+class CompilePluginIndySpec extends Specification {
+
+    @TempDir
+    File projectDir
+
+    private Project projectWithGroovy(Map extraProperties = [:]) {
+        File asf = new File(projectDir, '.asf.yaml')
+        asf.text = 'github: { labels: [grails] }\n'
+        File scriptDir = new File(projectDir, 'gradle')
+        scriptDir.mkdirs()
+        new File(scriptDir, 'groovy-compile-configscript.groovy').text = '// 
test fixture\n'
+        Project project = 
ProjectBuilder.builder().withProjectDir(projectDir).build()
+        extraProperties.each { key, value ->
+            project.extensions.extraProperties.set(key as String, value)
+        }
+        project.pluginManager.apply('groovy')
+        project.pluginManager.apply(CompilePlugin)
+        project
+    }
+
+    def "absent grailsIndy leaves compiler indy unset"() {
+        when:
+        Project project = projectWithGroovy()
+        GroovyCompile compile = project.tasks.named('compileGroovy', 
GroovyCompile).get()
+
+        then:
+        compile.groovyOptions.optimizationOptions.indy == null
+    }
+
+    def "grailsIndy true sets compiler indy"() {
+        when:
+        Project project = projectWithGroovy(grailsIndy: 'true')
+        GroovyCompile compile = project.tasks.named('compileGroovy', 
GroovyCompile).get()
+
+        then:
+        compile.groovyOptions.optimizationOptions.indy
+    }
+
+    def "grailsIndy false sets compiler indy off"() {
+        when:
+        Project project = projectWithGroovy(grailsIndy: 'false')
+        GroovyCompile compile = project.tasks.named('compileGroovy', 
GroovyCompile).get()
+
+        then:
+        !compile.groovyOptions.optimizationOptions.indy
+    }
+}
diff --git a/grails-doc/src/en/guide/introduction/whatsNew.adoc 
b/grails-doc/src/en/guide/introduction/whatsNew.adoc
index aa8b28f1b1..1786441b1a 100644
--- a/grails-doc/src/en/guide/introduction/whatsNew.adoc
+++ b/grails-doc/src/en/guide/introduction/whatsNew.adoc
@@ -32,6 +32,24 @@ Notable new features are included below.
 Grails {grailsMajorVersion} raises the standard build and runtime baseline to 
Java 21 and uses Gradle {gradleVersion}.
 The standard Grails BOM remains on Groovy {groovyVersion} and Spock 
{spockVersion}, while Micronaut-enabled Grails applications use 
Micronaut-specific BOM variants that align with Micronaut 5 and require JDK 25 
or later.
 
+==== Groovy invokedynamic enabled by default
+
+Grails 7 disabled Groovy invokedynamic for `GroovyCompile` tasks because of 
performance regressions on Groovy 4 
(https://github.com/apache/grails-core/issues/15293[#15293]).
+Grails 9 turns invokedynamic back on by default (`grails { indy = true }`).
+That matches Groovy 6's preferred dispatch and is the mode CI already 
exercises on the Groovy 6 line.
+
+To keep classic callsite bytecode:
+
+[source,groovy]
+.build.gradle
+----
+grails {
+    indy = false
+}
+----
+
+Non-indy builds on Groovy 6 need `org.apache.groovy:groovy-callsite` on the 
classpath. The Grails BOM already manages that coordinate.
+
 ==== Spring Boot 4.1 and Spring Framework 7
 
 Grails {grailsMajorVersion} is built on Spring Boot {springBootVersion} and 
Spring Framework {springVersion}.
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
index eba2054a63..ff6401e6ca 100644
--- 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
@@ -47,7 +47,7 @@ class GrailsExtension {
     GrailsExtension(Project project) {
         this.project = project
         this.pluginDefiner = new PluginDefiner(project)
-        this.indy = project.objects.property(Boolean).convention(false)
+        this.indy = project.objects.property(Boolean).convention(true)
         this.preserveParameterNames = 
project.objects.property(Boolean).convention(true)
         this.compileStatic = 
project.objects.newInstance(GrailsCompileStaticOptions)
         this.cliAutoProvision = 
project.objects.property(Boolean).convention(project.provider {
@@ -202,9 +202,9 @@ class GrailsExtension {
 
     /**
      * Whether to enable Groovy's invokedynamic (indy) bytecode instruction 
for dynamic Groovy method dispatch.
-     * Disabled by default to improve performance (see GitHub issue #15293).
-     * When enabled, Groovy uses JVM invokedynamic instead of traditional 
callsite caching.
-     * To enable invokedynamic in build.gradle: grails { indy = true }
+     * Enabled by default on Grails 9 / Groovy 6. Groovy 4+ already prefers 
indy; Grails 7 disabled it
+     * for performance (#15293). Re-evaluate that default now that the line is 
on Groovy 6.
+     * To opt out: grails { indy = false }
      */
     final Property<Boolean> indy
 
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
index 805deadf42..5a529a32ef 100644
--- 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
@@ -237,7 +237,7 @@ class GrailsGradlePlugin implements Plugin<Project> {
         // Configure indy and log status after evaluation so user's grails { } 
block has been applied
         GrailsExtension grailsExtension = 
project.extensions.findByType(GrailsExtension)
         project.afterEvaluate {
-            boolean indyEnabled = grailsExtension.indy.getOrElse(false)
+            boolean indyEnabled = grailsExtension.indy.getOrElse(true)
             Boolean preserveParameterNames = 
grailsExtension.preserveParameterNames.getOrNull()
 
             project.tasks.withType(GroovyCompile).configureEach { 
GroovyCompile c ->
@@ -249,9 +249,12 @@ class GrailsGradlePlugin implements Plugin<Project> {
                 }
             }
 
-            if (!indyEnabled) {
-                project.logger.info('Grails: Groovy invokedynamic (indy) is 
disabled to improve performance (see issue #15293).')
-                project.logger.info('        To enable invokedynamic: grails { 
indy = true } in build.gradle')
+            if (indyEnabled) {
+                project.logger.info('Grails: Groovy invokedynamic (indy) is 
enabled (Grails 9 default).')
+                project.logger.info('        To disable invokedynamic: grails 
{ indy = false } in build.gradle')
+            } else {
+                project.logger.info('Grails: Groovy invokedynamic (indy) is 
disabled.')
+                project.logger.info('        Non-indy Groovy 6 builds need 
org.apache.groovy:groovy-callsite on the classpath.')
             }
         }
     }
diff --git 
a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsExtensionSpec.groovy
 
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsExtensionSpec.groovy
index 1952c09ea5..27c3e912c4 100644
--- 
a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsExtensionSpec.groovy
+++ 
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsExtensionSpec.groovy
@@ -185,4 +185,27 @@ class GrailsExtensionSpec extends Specification {
         !extension.compileStatic.services.get()
         !extension.compileStatic.tagLibs.get()
     }
+
+    def "indy defaults to enabled"() {
+        given:
+        Project project = ProjectBuilder.builder().build()
+
+        when:
+        GrailsExtension extension = new GrailsExtension(project)
+
+        then:
+        extension.indy.get()
+    }
+
+    def "indy can be opted out"() {
+        given:
+        Project project = ProjectBuilder.builder().build()
+        GrailsExtension extension = new GrailsExtension(project)
+
+        when:
+        extension.indy = false
+
+        then:
+        !extension.indy.get()
+    }
 }

Reply via email to