GROOVY-11240: Tweak cleanup DFA cache of parser
diff --git a/build-logic/src/main/groovy/org.apache.groovy-core.gradle b/build-logic/src/main/groovy/org.apache.groovy-core.gradle
index 81d27ec..ed7be8e 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-core.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-core.gradle
@@ -191,7 +191,7 @@
}
tasks.withType(Test).configureEach {
- jvmArgs /*"-Dgroovy.attach.groovydoc=true", "-Dgroovy.attach.runtime.groovydoc=true",*/ "-Dgroovy.antlr4.cache.threshold=100"
+ jvmArgs /*"-Dgroovy.attach.groovydoc=true", "-Dgroovy.attach.runtime.groovydoc=true",*/ "-Dgroovy.antlr4.cache.threshold=0"
}
def generateGrammarSourceTask = tasks.named("generateGrammarSource") {
diff --git a/build-logic/src/main/groovy/org.apache.groovy-internal.gradle b/build-logic/src/main/groovy/org.apache.groovy-internal.gradle
index f538b42..773424b 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-internal.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-internal.gradle
@@ -77,7 +77,7 @@
}
tasks.withType(GroovyCompile).configureEach {
- groovyOptions.forkOptions.jvmArgs += ["-Dgroovy.antlr4.cache.threshold=100", "-Dgroovy.target.bytecode=${sharedConfiguration.groovyTargetBytecodeVersion.get()}" as String]
+ groovyOptions.forkOptions.jvmArgs += ["-Dgroovy.antlr4.cache.threshold=0", "-Dgroovy.target.bytecode=${sharedConfiguration.groovyTargetBytecodeVersion.get()}" as String]
groovyOptions.fork(memoryMaximumSize: sharedConfiguration.groovycMaxMemory.get())
groovyClasspath = configurations.groovyCompilerClasspath
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/AtnManager.java b/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/AtnManager.java
index 9e80d90..535413f 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/AtnManager.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/AtnManager.java
@@ -20,9 +20,15 @@
import org.antlr.v4.runtime.atn.ATN;
import org.apache.groovy.util.SystemUtil;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
+import java.lang.invoke.MethodHandles;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.logging.Logger;
/**
* Manage ATN to avoid memory leak
@@ -33,17 +39,65 @@
public static final ReentrantReadWriteLock.ReadLock READ_LOCK = RRWL.readLock();
private static final String DFA_CACHE_THRESHOLD_OPT = "groovy.antlr4.cache.threshold";
private static final long DFA_CACHE_THRESHOLD;
+ private final ReferenceQueue<AtnWrapper> atnWrapperReferenceQueue = new ReferenceQueue<>();
+ private AtnWrapperSoftReference atnWrapperSoftReference;
static {
- long t = SystemUtil.getLongSafe(DFA_CACHE_THRESHOLD_OPT, 64L);
- if (t <= 0) {
+ long t = SystemUtil.getLongSafe(DFA_CACHE_THRESHOLD_OPT, 0L);
+ if (t < 0) {
t = Long.MAX_VALUE;
}
DFA_CACHE_THRESHOLD = t;
}
- public abstract ATN getATN();
+ {
+ Thread cleanupThread = new Thread(() -> {
+ while (true) {
+ try {
+ Reference<? extends AtnWrapper> reference = atnWrapperReferenceQueue.remove();
+ if (reference instanceof AtnWrapperSoftReference && shouldClearDfaCache() && isSmartCleanupEnabled()) {
+ AtnWrapperSoftReference atnWrapperSoftReference = (AtnWrapperSoftReference) reference;
+ atnWrapperSoftReference.getAtnManager().getAtnWrapper(false).clearDFA();
+ }
+ } catch (Throwable t) {
+ Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
+ logger.warning(DefaultGroovyMethods.asString(t));
+ }
+ }
+ }, "Cleanup thread for DFA cache[" + this.getClass().getSimpleName() + "]");
+ cleanupThread.setDaemon(true);
+ cleanupThread.start();
+ }
+
+ private static boolean isSmartCleanupEnabled() {
+ return 0 == DFA_CACHE_THRESHOLD;
+ }
+
+ public ATN getATN() {
+ return getAtnWrapper().checkAndClear();
+ }
+
+ protected abstract AtnWrapper createAtnWrapper();
+
+ protected AtnWrapper getAtnWrapper() {
+ return getAtnWrapper(true);
+ }
+
+ private AtnWrapper getAtnWrapper(final boolean useSoftRef) {
+ if (!useSoftRef) {
+ return createAtnWrapper();
+ }
+
+ AtnWrapper atnWrapper;
+ synchronized (this) {
+ if (null == atnWrapperSoftReference || null == (atnWrapper = atnWrapperSoftReference.get())) {
+ atnWrapper = createAtnWrapper();
+ atnWrapperSoftReference = new AtnWrapperSoftReference(atnWrapper, this, atnWrapperReferenceQueue);
+ }
+ }
+ return atnWrapper;
+ }
protected abstract boolean shouldClearDfaCache();
@@ -56,7 +110,7 @@
}
public ATN checkAndClear() {
- if (!shouldClearDfaCache()) {
+ if (!shouldClearDfaCache() || isSmartCleanupEnabled()) {
return atn;
}
@@ -64,14 +118,31 @@
return atn;
}
+ clearDFA();
+
+ return atn;
+ }
+
+ public void clearDFA() {
WRITE_LOCK.lock();
try {
atn.clearDFA();
} finally {
WRITE_LOCK.unlock();
}
+ }
+ }
- return atn;
+ private static class AtnWrapperSoftReference extends SoftReference<AtnWrapper> {
+ private final AtnManager atnManager;
+
+ public AtnWrapperSoftReference(AtnWrapper referent, AtnManager atnManager, ReferenceQueue<? super AtnWrapper> q) {
+ super(referent, q);
+ this.atnManager = atnManager;
+ }
+
+ public AtnManager getAtnManager() {
+ return atnManager;
}
}
}
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/LexerAtnManager.java b/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/LexerAtnManager.java
index 2b7d560..fb93e40 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/LexerAtnManager.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/LexerAtnManager.java
@@ -18,7 +18,6 @@
*/
package org.apache.groovy.parser.antlr4.internal.atnmanager;
-import org.antlr.v4.runtime.atn.ATN;
import org.apache.groovy.parser.antlr4.GroovyLangLexer;
import org.apache.groovy.util.SystemUtil;
@@ -28,7 +27,6 @@
public class LexerAtnManager extends AtnManager {
private static final String GROOVY_CLEAR_LEXER_DFA_CACHE = "groovy.antlr4.clear.lexer.dfa.cache";
private static final boolean TO_CLEAR_LEXER_DFA_CACHE;
- private final AtnWrapper lexerAtnWrapper = new AtnManager.AtnWrapper(GroovyLangLexer._ATN);
public static final LexerAtnManager INSTANCE = new LexerAtnManager();
static {
@@ -36,8 +34,8 @@
}
@Override
- public ATN getATN() {
- return lexerAtnWrapper.checkAndClear();
+ protected AtnWrapper createAtnWrapper() {
+ return new AtnWrapper(GroovyLangLexer._ATN);
}
@Override
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/ParserAtnManager.java b/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/ParserAtnManager.java
index 9b74bcd..56e82df 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/ParserAtnManager.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/internal/atnmanager/ParserAtnManager.java
@@ -18,19 +18,17 @@
*/
package org.apache.groovy.parser.antlr4.internal.atnmanager;
-import org.antlr.v4.runtime.atn.ATN;
import org.apache.groovy.parser.antlr4.GroovyLangParser;
/**
* Manage ATN for parser to avoid memory leak
*/
public class ParserAtnManager extends AtnManager {
- private final AtnWrapper parserAtnWrapper = new AtnManager.AtnWrapper(GroovyLangParser._ATN);
public static final ParserAtnManager INSTANCE = new ParserAtnManager();
@Override
- public ATN getATN() {
- return parserAtnWrapper.checkAndClear();
+ protected AtnWrapper createAtnWrapper() {
+ return new AtnWrapper(GroovyLangParser._ATN);
}
@Override
diff --git a/src/spec/doc/performance-guide.adoc b/src/spec/doc/performance-guide.adoc
index b652505..0e9a568 100644
--- a/src/spec/doc/performance-guide.adoc
+++ b/src/spec/doc/performance-guide.adoc
@@ -27,7 +27,7 @@
|=======================================================================
| Option | Description | Default | Version | Example
| groovy.parallel.parse | Parsing groovy source files in parallel. | `false` util Groovy 4.0.0 | 3.0.5+ | -Dgroovy.parallel.parse=true
-| groovy.antlr4.cache.threshold | antlr4 relies on DFA cache heavily for better performance, so antlr4 will not clear DFA cache, thus OutOfMemoryError will probably occur. Groovy trades off parsing performance and memory usage, when the count of Groovy source files parsed hits the cache threshold, the DFA cache will be cleared. *Note:* `0` means never clearing DFA cache, so requiring bigger JVM heap size. Or set a greater value, e.g. 200 to clear DFA cache if threshold hits. **Note: ** the threshold specified is the count of groovy source files | `64` | 3.0.5+ | -Dgroovy.antlr4.cache.threshold=200
+| groovy.antlr4.cache.threshold | antlr4 relies on DFA cache heavily for better performance, so antlr4 will not clear DFA cache, thus OutOfMemoryError will probably occur. Groovy trades off parsing performance and memory usage, when the count of Groovy source files parsed hits the cache threshold, the DFA cache will be cleared. *Note:* `0` means managing the DFA cache automatically, `-1` means never clearing DFA cache, so requiring bigger JVM heap size. Or set a greater value, e.g. 200 to clear DFA cache if threshold hits. **Note: ** the threshold specified is the count of groovy source files | `64` | 3.0.5+ | -Dgroovy.antlr4.cache.threshold=200
| groovy.antlr4.sll.threshold | Parrot parser will try SLL mode and then try LL mode if SLL failed. But the more tokens to parse, the more likely SLL will fail. If SLL threshold hits, SLL will be skipped. Setting the threshold to `0` means never trying SLL mode, which is not recommended at most cases because SLL is the fastest mode though SLL is less powerful than LL. **Note: ** the threshold specified is the token count | `-1` (disabled by default) | 3.0.9+ | -Dgroovy.antlr4.sll.threshold=1000
| groovy.antlr4.clear.lexer.dfa.cache | Clear the DFA cache for lexer. The DFA cache for lexer is always small and important for parsing performance, so it's strongly recommended to leave it as it is until OutOfMemoryError will truly occur | `false`| 3.0.9+ | -Dgroovy.antlr4.clear.lexer.dfa.cache=true
|=======================================================================