Skip to content

build: disable Groovy invokedynamic for the Grails 8 compile - #16178

Merged
jamesfredley merged 7 commits into
8.0.xfrom
fix/disable-groovy-indy-build
Sep 4, 2026
Merged

build: disable Groovy invokedynamic for the Grails 8 compile#16178
jamesfredley merged 7 commits into
8.0.xfrom
fix/disable-groovy-indy-build

Conversation

@jamesfredley

@jamesfredley jamesfredley commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

If Grails 9 on Groovy 6, this will flip the other direction: #16165

Groovy 5's compiler default is invokedynamic on. In this repo, only modules that apply the Grails Gradle plugin inherited grails.indy = false. Everything else (grails-gradle, grails-forge, build-logic, and published framework modules that only apply org.apache.grails.buildsrc.compile) compiled with indy on. That left mixed bytecode and the #15293 performance regression in artifacts that never went through the Grails plugin.

This PR turns indy off for the entire Grails 8 compile, from one shared default, and applies it in all three independent builds.

How it is centralized

  • CompilePlugin (org.apache.grails.buildsrc.compile) now sets optimizationOptions.indy = false. That is the historical GroovyCompile convention plugin used by framework, grails-gradle, and grails-forge modules.
  • gradle/groovy-indy.gradle is applied from subprojects in:
    • grails-core (build.gradle)
    • grails-gradle (grails-gradle/build.gradle)
    • grails-forge (grails-forge/build.gradle)
  • build-logic cannot apply CompilePlugin to itself (it compiles that plugin). plugins and docs-core set the same default locally.
  • CI can still opt in with -PgrailsIndy=true (same property as grails-extension-gradle-config.gradle).
  • Applications are unchanged: GrailsExtension.indy still defaults to false, and grails { indy = true } / the GraalVM native convention still win in afterEvaluate.

Grails 9 / Groovy 6 can flip this default the other way when indy is ready.

Verification

  • CompilePluginSpec asserts compileGroovy / compileTestGroovy are indy=false by default and true with -PgrailsIndy=true
  • :build-logic:test and :grails-docs-core:test pass
  • help configures cleanly in grails-core, grails-gradle, and grails-forge
  • javap on grails-encoder CodecMetaClassSupport shows classic CallSiteArray, not invokedynamic dispatch

Fixes #15293 (build-side: stop compiling the framework with Groovy's indy default)

See also #15431 (warning noise when indy is disabled; not changed here).

Copilot AI lite review requested due to automatic review settings August 19, 2026 23:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Centralizes Groovy invokedynamic (“indy”) defaults for the Grails 8 build so all framework/related modules compile with indy disabled by default (with an opt-in via -PgrailsIndy=true), avoiding mixed bytecode across the monorepo and addressing the performance regression discussed in #15293.

Changes:

  • Add a shared Gradle script to set GroovyCompile.groovyOptions.optimizationOptions.indy consistently across the independent builds (grails-core, grails-gradle, grails-forge).
  • Update org.apache.grails.buildsrc.compile (CompilePlugin) to default indy to false, with a grailsIndy property override.
  • Add a TestKit spec asserting the default/override behavior for Groovy compile tasks.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
build.gradle Applies the shared gradle/groovy-indy.gradle script to all subprojects in grails-core.
grails-gradle/build.gradle Applies the shared indy-default script to all grails-gradle subprojects.
grails-forge/build.gradle Applies the shared indy-default script to all grails-forge subprojects.
gradle/groovy-indy.gradle New shared script that sets indy default (with -PgrailsIndy opt-in).
gradle/grails-extension-gradle-config.gradle Clarifies in comments that non-plugin modules inherit the same default via CompilePlugin.
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy Sets GroovyCompile optimization option indy from grailsIndy property (default false).
build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginSpec.groovy New TestKit spec validating indy default and property override.
build-logic/plugins/build.gradle Sets local indy default for build-logic/plugins (can’t apply CompilePlugin to itself).
build-logic/docs-core/build.gradle Sets local indy default for docs-core (doesn’t apply CompilePlugin).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread build-logic/docs-core/build.gradle Outdated
Comment thread gradle/groovy-indy.gradle Outdated
Comment thread build-logic/plugins/build.gradle Outdated
Comment thread gradle/grails-extension-gradle-config.gradle Outdated
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.7232%. Comparing base (17b9ded) to head (6fce5e1).
⚠️ Report is 8 commits behind head on 8.0.x.

Files with missing lines Patch % Lines
.../grails/web/databinding/GrailsWebDataBinder.groovy 0.0000% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #16178        +/-   ##
==================================================
- Coverage     54.8357%   54.7232%   -0.1125%     
- Complexity      20523      20532         +9     
==================================================
  Files            2104       2104                
  Lines          101102     101149        +47     
  Branches        17932      17966        +34     
==================================================
- Hits            55440      55352        -88     
- Misses          37771      37774         +3     
- Partials         7891       8023       +132     
Files with missing lines Coverage Δ
.../groovy/grails/databinding/SimpleDataBinder.groovy 74.3210% <100.0000%> (-0.1840%) ⬇️
...ovy/org/grails/datastore/gorm/GormStaticApi.groovy 76.1290% <100.0000%> (-0.2464%) ⬇️
.../grails/web/databinding/GrailsWebDataBinder.groovy 30.0341% <0.0000%> (-0.0513%) ⬇️

... and 58 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. we should be using composition and not using the subprojects { } block in gradle files
  2. we should be exclusively applying this to the CompilePlugin since it's supposed to be applied to any project that's being published.
  3. for gradle plugins that are not published, why do we need to even toggle indy? We should use what Gradle's default is.

Comment thread build-logic/docs-core/build.gradle Outdated
Comment thread build-logic/plugins/build.gradle Outdated
Comment thread gradle/groovy-indy.gradle Outdated
Comment thread grails-forge/build.gradle Outdated
Comment thread grails-gradle/build.gradle Outdated
Groovy 5 defaults indy on. Only modules that apply the Grails Gradle
plugin inherited grails.indy=false, so published framework artifacts
were mixed. Centralize indy=false in CompilePlugin and apply
gradle/groovy-indy.gradle from the grails-core, grails-gradle, and
grails-forge builds. CI can still opt in with -PgrailsIndy=true.

See #15293

Assisted-by: Sisyphus:grok-4.6
With indy off, log.debug inside GormStaticApi.count()'s session
callback was dispatched as Domain.debug(...). Capture the @slf4j
logger in a local first. Also trim -PgrailsIndy the same way as
CompilePlugin and apply the shared groovy-indy script from
build-logic.

Assisted-by: Sisyphus:grok-4.6
Class.newInstance(Map) is not selected under @CompileStatic when
invokedynamic is disabled. Use InvokerHelper.invokeConstructorOf with
an explicit Object[] so nested Map-constructor types still bind.
Also avoid `null as boolean` in the Map-constructor test fixture,
which Groovy 5 throws on without indy after unbindable properties
are filtered from constructor arguments.

Assisted-by: Sisyphus:grok-4.6
Honor review: drop groovy-indy.gradle and subprojects applies. Unpublished build-logic uses Gradle's Groovy default.
@jamesfredley
jamesfredley force-pushed the fix/disable-groovy-indy-build branch from b224f33 to 505542f Compare September 2, 2026 22:57
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Addressed @jdaugherty review: indy-off is only in CompilePlugin (published artifacts). Removed groovy-indy.gradle and the subprojects applies. Unpublished build-logic/docs-core and plugins use Gradle's Groovy default. Rebased onto latest 8.0.x.

@jamesfredley
jamesfredley force-pushed the fix/disable-groovy-indy-build branch from b16e242 to 758ce89 Compare September 4, 2026 21:50
@jamesfredley
jamesfredley force-pushed the fix/disable-groovy-indy-build branch from 758ce89 to 6fce5e1 Compare September 4, 2026 22:49
@jamesfredley
jamesfredley merged commit 41259bf into 8.0.x Sep 4, 2026
78 of 79 checks passed
@jamesfredley
jamesfredley deleted the fix/disable-groovy-indy-build branch September 4, 2026 22:52
This was referenced Sep 4, 2026
jamesfredley added a commit that referenced this pull request Sep 4, 2026
Take #16178's CompilePlugin -PgrailsIndy toggle and flip the default
to true so Grails 9 / Groovy 6 framework modules match app-level
grails { indy } (already default-on). CI can still pass
-PgrailsIndy=false.

Assisted-by: Sisyphus:grok-4.6
@testlens-app

testlens-app Bot commented Sep 4, 2026

Copy link
Copy Markdown

🚨 TestLens detected 1 failed test 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

CI / Functional Tests (Java 25, indy=false) > :grails-test-examples-scaffolding:integrationTest

Test Runs Flakiness
UserControllerSpec > User list 4% 🟡

🏷️ Commit: 6fce5e1
▶️ Tests: 62529 executed
🟡 Checks: 57/84 completed

Test Failures

UserControllerSpec > User list (:grails-test-examples-scaffolding:integrationTest in CI / Functional Tests (Java 25, indy=false))
geb.waiting.WaitTimeoutException: condition did not pass in 30 seconds (failed with exception)
	at geb.waiting.Wait.waitFor(Wait.groovy:128)
	at geb.waiting.DefaultWaitingSupport.doWaitFor(DefaultWaitingSupport.groovy:55)
	at geb.waiting.DefaultWaitingSupport.waitFor(DefaultWaitingSupport.groovy:41)
	at geb.Page.waitFor(Page.groovy:120)
	at com.example.pages.LoginPage.login(LoginPage.groovy:39)
	at com.example.UserControllerSpec.User list(UserControllerSpec.groovy:48)
Caused by: Assertion failed: 

title != pageTitle && $('input', name: 'username').empty
|     |  |         |
|     |  |         false
|     |  'Please sign in'
|     false
'Please sign in'

	at com.example.pages.LoginPage.login_closure1(LoginPage.groovy:39)
	at com.example.pages.LoginPage.login_closure1(LoginPage.groovy)
	at geb.waiting.Wait.waitFor(Wait.groovy:117)
	... 5 more

Rerun Controls

Note

Checks are currently running using the configuration below.

Select tests to mute in this pull request:

🔲 UserControllerSpec > User list

Reuse successful test results:

🔲 ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

🔲 Rerun jobs


Learn more about TestLens at testlens.app/docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Groovy - invoke dynamic performance problems

3 participants