sbglasius commented on code in PR #16376:
URL: https://github.com/apache/grails-core/pull/16376#discussion_r4073079661


##########
gradle.properties:
##########
@@ -96,8 +93,23 @@ org.gradle.daemon=true
 # other native memory, the Gradle client, and forked Java/Groovy compiler 
workers (which
 # CompilePlugin gives their own -Xmx2G). Treat it as a floor when sizing a 
runner.
 #
-# On the 4-CPU / ~16 GB Linux and Windows runners that floor is 5G + 4x768m = 
8G, which
-# fits. On the 3-CPU / ~7 GB macOS runner it is 5G + 3x768m = 7.25G, which 
does not - so
-# .github/workflows/gradle.yml caps --max-workers there to reduce memory 
pressure, rather
-# than shrinking this daemon and slowing groovydoc.
-org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx5G
+# Documentation does NOT run on this heap. Every groovydoc task is launched as 
a JVM of its
+# own by GroovydocEnhancerPlugin, and the user guide in a worker process by 
PublishGuideTask.
+# That is what lets this number be 3G: before, groovydoc kept a Groovy runtime 
per documented
+# module alive inside the daemon for the whole build, which needed 5G and 
still exhausted it
+# on the smallest runner.
+#
+# 3G is measured, not guessed. `build :grails-shell-cli:installDist groovydoc 
-PskipTests
+# --max-workers=2` - 3253 tasks, the macOS job's graph without test execution 
- runs it with
+# ZERO full GCs; mixed collections settle the daemon at about 2.0 GB 
throughout. Re-measure
+# with -Xlog:gc* before changing it, and read the MIXED collections: a 
young-only "after GC"
+# figure does not touch the old generation and reads about 700 MB higher than 
the truth.
+# Raising this on a memory-constrained runner can backfire - a larger heap 
lets G1 defer
+# collection, so the daemon's resident size grows to meet it and leaves the 
forked doc JVMs
+# and test forks less room, not more.
+org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx3G

Review Comment:
   **The 3G figure is measured without test execution, but every CI job runs 
the full `build`.**
   
   The comment above says so explicitly: `build :grails-shell-cli:installDist 
groovydoc -PskipTests` - "the macOS job's graph without test execution". Moving 
groovydoc and the guide out of the daemon genuinely removes their heap, but 
test execution adds daemon-side load that measurement never exercised - Gradle 
streams and buffers test events across ~all modules, generates the aggregated 
HTML/XML test reports, and runs JaCoCo/report aggregation in the daemon.
   
   So a 40% heap cut validated on a tests-free graph may move the OOM from 
`publishGuide` to `:test` reporting / `check` aggregation rather than remove 
it. Worth re-measuring with tests in the graph (or keeping a higher floor until 
that run is clean) before this lands on the macOS job, which is the one with no 
headroom.



##########
build-logic/docs-core/src/main/groovy/grails/doc/gradle/PublishGuideTask.groovy:
##########
@@ -110,8 +133,11 @@ class PublishGuideTask extends DefaultTask {
         propertiesFiles = objects.fileCollection()
         sourceDir = 
objects.directoryProperty().convention(project.layout.projectDirectory.dir('src'))
         resourcesDir = 
objects.directoryProperty().convention(project.layout.projectDirectory.dir('resources'))
-        macros = objects.listProperty(Object).convention([])
+        macros = objects.listProperty(String).convention([])
         targetDir = 
objects.directoryProperty().convention(project.layout.buildDirectory.dir('docs'))
+        maxHeapSize = objects.property(String)

Review Comment:
   **`guideMaxHeapSize` is wired as the *convention*, so a build script 
silently beats the command line** - the opposite of the javadoc just above and 
of what the groovydoc side does.
   
   A convention only applies while nothing has been set explicitly. Once any 
build script does:
   
   ```groovy
   publishGuide.maxHeapSize = '2g'
   ```
   
   then `./gradlew publishGuide -PguideMaxHeapSize=4g` keeps `2g`, the guide 
still OOMs, and nothing says why. The groovydoc path deliberately does the 
reverse - `GroovydocEnhancerPlugin.resolveMaxHeapSize` reads the project 
property first and only falls back to the extension - and both the javadoc at 
line 108 ("A `guideMaxHeapSize` project property beats whatever is set here") 
and `gradle.properties` ("either beats the build script") promise the groovydoc 
behaviour.
   
   Resolving it in the task action the same way `resolveMaxHeapSize` does would 
make the two consistent.
   
   Relatedly: `gradle.properties` says the doc JVM heap is "raised ... by 
grails-doc for the guide", but `grails-doc/build.gradle` never sets 
`maxHeapSize` on `publishGuide` - the guide runs on the `1500m` default.



##########
build-logic/docs-core/src/main/groovy/grails/doc/gradle/PublishGuideTask.groovy:
##########
@@ -124,81 +150,39 @@ class PublishGuideTask extends DefaultTask {
     }
 
     @TaskAction
-    def publishGuide() {
-        Properties combinedProperties = new Properties()
-
-        File workingDir = 
Files.createTempDirectory('grails-doc-publish-guide').toFile()
-
-        File resources = resourcesDir.get().asFile
-        File docProperties = new File(resources, 'doc.properties')
-        if (docProperties.exists()) {
-            docProperties.withInputStream { input ->
-                combinedProperties.load(input)
-            }
-        }
-
-        // Add properties from any optional properties files too.
-        for (File f : propertiesFiles) {
-            f.withInputStream { input ->
-                combinedProperties.load(input)
-            }
-        }
-        combinedProperties.putAll(properties.get())
-        combinedProperties.putAll(propertiesWithFilePaths.get())
-
-        File apiDir = targetDir.get().asFile
-        apiDir.deleteDir()
-        apiDir.mkdirs()
-
-        def publisher = new DocPublisher(sourceDir.get().asFile, apiDir)
-        publisher.ant = ant
-        publisher.asciidoc = asciidoc
-        publisher.workDir = workingDir
-        publisher.apiDir = apiDir
-        publisher.language = language.getOrElse('')
-        publisher.sourceRepo = sourceRepo.getOrElse('')
-        publisher.images = new File(resources, 'img')
-        publisher.css = new File(resources, 'css')
-        publisher.fonts = new File(resources, 'fonts')
-        publisher.js = new File(resources, 'js')
-        publisher.style = new File(resources, 'style')
-        publisher.version = combinedProperties['grails.version']
-
-        // Override doc.properties properties with their language-specific 
counterparts (if
-        // those are defined). You just need to add entries like es.title or 
pt_PT.subtitle.
-        if (language.isPresent()) {
-            String lang = language.get()
-            def pos = lang.size() + 1
-            def languageProps = combinedProperties.findAll { k, v -> 
k.startsWith("${lang}.") }
-            languageProps.each { k, v -> combinedProperties[k[pos..-1]] = v }
+    void publishGuide() {
+        // Everything is read into locals first. Both the fork options and the 
work parameters
+        // have members of their own named like this task's properties - 
maxHeapSize,
+        // properties - and inside the configuration closures those would win.
+        String workerHeap = this.maxHeapSize.get()
+        String languageValue = this.language.getOrNull()
+        String sourceRepoValue = this.sourceRepo.getOrNull()
+        Boolean asciidocValue = this.asciidoc.get()
+        Map<String, Object> engineProperties = this.properties.get()
+        Map<String, File> filePathProperties = 
this.propertiesWithFilePaths.get()
+        FileCollection propertiesFileValues = this.propertiesFiles
+        Directory sourceDirValue = this.sourceDir.get()
+        Directory resourcesDirValue = this.resourcesDir.get()
+        Directory targetDirValue = this.targetDir.get()
+        List<String> macroNames = this.macros.get()
+        Boolean verboseAntValue = this.verboseAnt.get()
+
+        WorkQueue queue = workerExecutor.processIsolation { spec ->

Review Comment:
   **A `processIsolation` worker is a pooled worker daemon, not a per-task 
process** - so the guide JVM is not released when `publishGuide` finishes.
   
   Gradle keeps process-isolation workers in `WorkerDaemonClientsManager` and 
reuses them for compatible work; they are torn down when the build session ends 
or the daemon expires them, not at the end of the task action. That means a JVM 
sized by `maxHeapSize` (1500m) stays resident while the rest of the 
documentation chain runs - `aggregateGroovydoc` at 3g, 
`aggregateDataMappingGroovydoc` at 2g - which is the memory pressure this PR is 
trying to remove.
   
   The groovydoc side does not have this problem: `ExecOperations.javaexec` 
really does exit with the task. Worth either aligning the guide on `javaexec` 
too, or softening the class docs on `PublishGuideWorkAction` - they currently 
read as though the burst is handed straight back to the OS.



##########
build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GroovydocEnhancerPluginSpec.groovy:
##########
@@ -51,4 +89,74 @@ class GroovydocEnhancerPluginSpec extends Specification {
         groovydocFiles.any { it.name == runtimeOnlyJar.name }
         groovydocFiles.any { it.name == compileOnlyJar.name }
     }
+
+    void 'groovydoc documents the sources with the groovy the project builds 
against'() {
+        given: 'a project whose documentation classpath pins its own groovy'
+        writeGroovydocProject()
+
+        when: 'groovydoc runs'
+        def result = GradleRunner.create()
+                .withProjectDir(projectDir)
+                .withArguments('groovydoc')
+                .withPluginClasspath()
+                .build()
+
+        then: 'the task succeeds and documents the source'
+        result.task(':groovydoc').outcome == TaskOutcome.SUCCESS
+        File documented = new File(projectDir, 
'build/docs/groovydoc/com/example/Documented.html')
+        documented.exists()
+
+        and: "the project's groovy generated it, not the older one Gradle runs 
build logic on"
+        documented.text.contains("Generated by groovydoc (${groovyVersion})")
+    }
+
+    void 'the groovydoc heap is the forked jvm heap, not the daemon heap'() {
+        given: 'a project that would document happily on the daemon heap'
+        writeGroovydocProject()
+
+        when: 'groovydoc is given a heap no JVM can start with'
+        def result = GradleRunner.create()
+                .withProjectDir(projectDir)
+                .withArguments('groovydoc', '-PgroovydocMaxHeapSize=1m')
+                .withPluginClasspath()
+                .buildAndFail()
+
+        then: 'a JVM refused to start on it - which only a forked JVM could 
report'
+        result.task(':groovydoc').outcome == TaskOutcome.FAILED
+        result.output.contains('Too small maximum heap')

Review Comment:
   **These assertions pin HotSpot's exact wording, and the new fixtures pull 
this spec onto the network.**
   
   `'Too small maximum heap'` and `'Error occurred during initialization of 
VM'` are HotSpot's phrasing for `-Xmx1m`. On a JVM that words it differently - 
or if HotSpot rewords it - the test fails while the plugin is correct. 
Asserting on the forked JVM's non-zero exit (the `ExecException` surfacing as 
the task failure) proves the same thing - that the heap setting reached a 
*separate* JVM - without depending on a message string.
   
   Separately, `writeGroovydocProject` declares `repositories { mavenCentral() 
}` and real Groovy dependencies, so `:build-logic:plugins:test` - previously a 
fast ProjectBuilder-only spec - now needs network access and a populated 
dependency cache. In an offline or air-gapped build it fails at dependency 
resolution rather than reporting a plugin defect.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to