Skip to content

Improve project dependency substitution logic - #16069

Open
matrei wants to merge 2 commits into
8.0.xfrom
improve-gradle-conf-time
Open

Improve project dependency substitution logic#16069
matrei wants to merge 2 commits into
8.0.xfrom
improve-gradle-conf-time

Conversation

@matrei

@matrei matrei commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

This cuts the Gradle configuration time roughly in half.

@matrei matrei changed the title build: improve project dependency substitution logic in functional te… Improve project dependency substitution logic Jul 30, 2026
…st configuration

This speeds up the configuration phase.
@matrei
matrei force-pushed the improve-gradle-conf-time branch from 1872843 to cc471e4 Compare July 30, 2026 08:59
@matrei
matrei marked this pull request as ready for review July 30, 2026 13:14
@matrei
matrei requested a review from Copilot July 30, 2026 13:14
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.8614%. Comparing base (301b40c) to head (d4945db).
⚠️ Report is 1 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #16069        +/-   ##
==================================================
- Coverage     53.1945%   51.8614%   -1.3331%     
- Complexity      17798      18111       +313     
==================================================
  Files            1980       2046        +66     
  Lines           92519      96274      +3755     
  Branches        16075      16727       +652     
==================================================
+ Hits            49215      49929       +714     
- Misses          36076      38974      +2898     
- Partials         7228       7371       +143     

see 69 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.

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

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Speeds up Gradle configuration (notably functional test setup) by precomputing dependency-substitution metadata for eligible subprojects and reusing it across configuration resolution.

Changes:

  • Precomputes a filtered list of substitutable subprojects (including resolved artifact IDs) once, instead of recalculating per-configuration.
  • Refactors the dependency substitution loop to iterate over the precomputed substitution metadata.
  • Keeps special-case substitutions (BOM platform + grails-geb test-fixtures variant) while applying the new precomputed structure.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

for (def substitution : substitutableProjects) {
def possibleProject = substitution.project
def substitutedArtifact = "$possibleProject.group:${substitution.artifactId}"
//TODO: This does not handle libraries that are both test fixtures & a libraries like grails-data-mongodb,
Comment on lines +20 to +29
def substitutableProjects = rootProject.subprojects
.findAll { !(it.name in testProjects) && !(it.name in docProjects) && !(it.name in cliProjects) }
.each { project.evaluationDependsOn(it.path) }
.collect {
project.evaluationDependsOn(it.path)
return [
project: it,
artifactId: it.findProperty('pomArtifactId') ?: it.name,
cliArtifactId: it.findProperty('cliArtifactId')
]
}
Comment thread gradle/functional-test-config.gradle Outdated
Comment on lines +22 to +29
.collect {
project.evaluationDependsOn(it.path)
return [
project: it,
artifactId: it.findProperty('pomArtifactId') ?: it.name,
cliArtifactId: it.findProperty('cliArtifactId')
]
}

@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.

Verified this is behavior-preserving and that the performance claim holds.

Configuration time (./gradlew help, warm daemon, 3 runs each, --profile → "Configuring Projects"): 7.95s → 3.69s (−54%); total wall time 17.3s → 10.8s. "Roughly in half" checks out.

Equivalence checks

  • Snapshot vs. lazy read — instrumented the precomputed snapshot and compared each entry against the value the previous code would have observed later, at resolution time: 134 substitutable projects × 104 applying test projects, zero mismatches (9 projects carry a cliArtifactId, 7 a custom pomArtifactId).
  • Resolved graph:grails-test-examples-app1:dependencies --configuration testRuntimeClasspath is identical. A first cross-checkout comparison showed grails-core-cli and grails-console gaining a requested version, but a controlled same-directory A/B (base version of this file swapped into the PR worktree) reproduced the PR output exactly, so that delta was checkout/build state, not this change.
  • Published BOM:grails-bom:generatePomFileForMavenPublication output is byte-identical, including all 9 *-cli constraints.
  • Flat pathsproject(":$name") stays safe: all 134 substitutable projects satisfy path == ":$name", so hoisting the filter introduces no nested-path regression.

No correctness issues found. Two non-blocking notes inline.

Comment thread gradle/functional-test-config.gradle Outdated
return [
project: it,
artifactId: it.findProperty('pomArtifactId') ?: it.name,
cliArtifactId: it.findProperty('cliArtifactId')

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.

This moves the cliArtifactId read from lazy (per configuration, when the substitution rules are registered) to eager (immediately after evaluationDependsOn). That matters because cliArtifactId is only set inside an afterEvaluate in GrailsCliArtifactGradlePlugin, which gradle/cli-companion-bom-constraints.gradle documents as the reason it uses a lazy withDependencies window.

It is correct as written — evaluationDependsOn flushes the target's afterEvaluate before returning, and I confirmed the snapshot matches the late value for all 134 substitutable projects across all 104 applying test projects with zero mismatches.

The only thing I would add is a short comment recording that invariant. The snapshot is now silently order-dependent: if cliArtifactId ever moves to a hook later than afterEvaluate, or gets set cross-project, the cli substitutions would quietly stop being registered rather than fail loudly, which is an unpleasant failure mode to debug.

Comment thread gradle/functional-test-config.gradle Outdated
def substitutableProjects = rootProject.subprojects
.findAll { !(it.name in testProjects) && !(it.name in docProjects) && !(it.name in cliProjects) }
.each { project.evaluationDependsOn(it.path) }
.collect {

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.

Optional follow-up, not a blocker. This snapshot is rebuilt independently by each of the 104 test projects that apply this script, so the findAll plus 134 evaluationDependsOn/findProperty calls all run 104 times over.

Computing it once and sharing it (keeping a cheap per-project evaluationDependsOn loop so evaluation ordering is unchanged) measured a further ~10% off configuration time, 3.69s → 3.32s, with identical resolution output.

For completeness, I also tried precomputing the "$group:$artifactId" coordinate strings into the snapshot so the loop body does no GString work at all — that was only 3.69s → 3.53s, near noise, so it is not worth doing for performance alone. The bulk of the win is already in this PR.

@testlens-app

testlens-app Bot commented Jul 31, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: d4945db
▶️ Tests: 39497 executed
⚪️ Checks: 60/60 completed


Learn more about TestLens at testlens.app.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants