daniellansun opened a new pull request, #3228: URL: https://github.com/apache/groovy/pull/3228
https://issues.apache.org/jira/browse/GROOVY-11726 reports that `GroovyClassLoader.parseClass(GroovyCodeSource, boolean)` roughly doubled in wall time for the same script batch between Groovy 3.0.13 and 4.0.24. ## Measurement JDK 21.0.12. Representative dynamic scripts (properties, closures, GStrings, maps), unique sources so the source cache cannot skip compilation. Sequential `parseClass` did not show a 2× gap: | Runtime | Batch | Wall time | |---|---|---| | Groovy 3.0.13 | 600 scripts, 8 methods | 3527 ms | | Groovy 4.0.24 | same | 3779 ms | | Groovy 3.0.13, `-Dgroovy.antlr4.cache.threshold=-1` | same | 3090 ms | | Groovy 4.0.24, same flag | same | 3360 ms | Indy on or off, an 80-jar classpath, and those jars on the parent loader stayed within a few percent. JFR on both release jars shows the same hot methods: lexer ATN simulation, then `ClassNode` work and class generation. Groovy 3.0.13's ASM cannot read JDK 21 class files, so `@CompileStatic` against JDK classes was not compared. On this master, one thread compiling 80 heavier scripts took 967 ms, against 1077 ms for Groovy 4.0.24 on the same harness. ## What the batch was waiting on `parseClass` compiled inside `FlexibleCache.getAndPut`. That method holds its write lock for the whole value-provider call, so every compilation on one `GroovyClassLoader` waited for every other one, including distinct sources. On Groovy 4.0.24 a 4-thread batch of 80 scripts took 1101 ms, against 1077 ms on one thread. ## Change Compile outside that lock. After a cache miss, `doParseClass` runs, then `getAndPut` only publishes the class already built; the first publisher for a key is kept. `definePackage` tolerates a concurrent definition of the same package. `bugs.Groovy11726` checks a cache hit, an uncached recompile, and that four threads overlap inside conversion. Before this change the overlap assertion failed with "overlapped 1 at a time". ## After Same 80-script harness on this commit: 967 ms on one thread, 690 ms and 680 ms on four threads. Shared parser DFA state still limits scaling, so four threads are not four times faster. ## Residual risk Two threads that miss the cache for the same source can both compile; only one class is stored. A cycle in which script A loads script B while B loads A is no longer serialized by the global lock. Independent batches are the case this changes. `IllegalArgumentException` from `definePackage` is ignored only when that package is already defined. `./gradlew :test --tests bugs.Groovy11726 --tests groovy.lang.GroovyClassLoaderTest` and `./gradlew lintGate` passed locally. ### Testing - `bugs.Groovy11726` -- 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]
