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() + } }
