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]

Reply via email to