diff --git a/bin/jmeter.properties b/bin/jmeter.properties index bac04e85bc9..05235ccd8f6 100644 --- a/bin/jmeter.properties +++ b/bin/jmeter.properties @@ -1235,6 +1235,9 @@ view.results.tree.renderers_order=.RenderAsText,.RenderAsRegexp,.RenderAsBoundar # Used by JSR-223 elements # Size of compiled scripts cache #jsr223.compiled_scripts_cache_size=100 +# Configurable options on the compiled scripts cache, overrides the jsr223.compiled_scripts_cache_size property +# See com.github.benmanes.caffeine.cache.Caffeine API for details +jsr223.compiled_scripts_cache_spec=maximumSize=100,recordStats #--------------------------------------------------------------------------- # Classpath configuration diff --git a/src/bom-thirdparty/build.gradle.kts b/src/bom-thirdparty/build.gradle.kts index 4d54c9168c7..19afa8a052d 100644 --- a/src/bom-thirdparty/build.gradle.kts +++ b/src/bom-thirdparty/build.gradle.kts @@ -47,7 +47,7 @@ dependencies { api("com.fasterxml.jackson.core:jackson-databind:2.16.1") api("com.fifesoft:rsyntaxtextarea:3.3.4") api("com.formdev:svgSalamander:1.1.4") - api("com.github.ben-manes.caffeine:caffeine:2.9.3") + api("com.github.ben-manes.caffeine:caffeine:3.1.8") api("com.github.weisj:darklaf-core:2.7.3") api("com.github.weisj:darklaf-extensions-rsyntaxarea:0.3.4") api("com.github.weisj:darklaf-property-loader:2.7.3") diff --git a/src/core/src/main/java/org/apache/jmeter/util/JSR223TestElement.java b/src/core/src/main/java/org/apache/jmeter/util/JSR223TestElement.java index c41e9f755a5..7952d4f6d07 100644 --- a/src/core/src/main/java/org/apache/jmeter/util/JSR223TestElement.java +++ b/src/core/src/main/java/org/apache/jmeter/util/JSR223TestElement.java @@ -46,12 +46,13 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.stats.CacheStats; /** * Base class for JSR223 Test elements */ public abstract class JSR223TestElement extends ScriptingTestElement - implements Serializable, TestStateListener + implements Serializable, TestStateListener { private static final long serialVersionUID = 233L; @@ -59,11 +60,12 @@ public abstract class JSR223TestElement extends ScriptingTestElement /** * Cache of compiled scripts */ - private static final Cache COMPILED_SCRIPT_CACHE = - Caffeine - .newBuilder() - .maximumSize(JMeterUtils.getPropDefault("jsr223.compiled_scripts_cache_size", 100)) - .build(); + private static Cache COMPILED_SCRIPT_CACHE; + + /** + * Used for locking cache initialization + */ + private static final Object lock = new Object(); /** * Lambdas can't throw checked exceptions, so we wrap cache loading failure with a runtime one. @@ -79,11 +81,11 @@ public synchronized Throwable fillInStackTrace() { } } - /** If not empty then script in ScriptText will be compiled and cached */ - private String cacheKey = ""; + /** If JSR223 element has checkbox 'Cache compile' checked then script in ScriptText will be compiled and cached */ + private String cacheChecked = ""; - /** md5 of the script, used as an unique key for the cache */ - private ScriptCacheKey scriptMd5; + /** Used as an unique key for the cache */ + private ScriptCacheKey scriptCacheKey; /** * Initialization On Demand Holder pattern @@ -99,7 +101,7 @@ private LazyHolder() { * @return ScriptEngineManager singleton */ public static ScriptEngineManager getInstance() { - return LazyHolder.INSTANCE; + return LazyHolder.INSTANCE; } protected JSR223TestElement() { @@ -203,13 +205,14 @@ protected Object processFileOrScript(ScriptEngine scriptEngine, final Bindings p return scriptEngine.eval(fileReader, bindings); } } - CompiledScript compiledScript; - ScriptCacheKey newCacheKey = - ScriptCacheKey.ofFile(getScriptLanguage(), scriptFile.getAbsolutePath(), scriptFile.lastModified()); - compiledScript = getCompiledScript(newCacheKey, key -> { + computeScriptCacheKey(scriptFile); + CompiledScript compiledScript = getCompiledScript(scriptCacheKey, key -> { try (BufferedReader fileReader = Files.newBufferedReader(scriptFile.toPath())) { return ((Compilable) scriptEngine).compile(fileReader); } catch (IOException | ScriptException e) { + if (logger.isDebugEnabled()) { + logger.warn("Cache missed access: for file script: '{}' for element named: '{}'", scriptFile.getAbsolutePath(), getName()); + } throw new ScriptCompilationInvocationTargetException(e); } }); @@ -217,17 +220,31 @@ protected Object processFileOrScript(ScriptEngine scriptEngine, final Bindings p } String script = getScript(); if (StringUtilities.isNotEmpty(script)) { - if (supportsCompilable && - !ScriptingBeanInfoSupport.FALSE_AS_STRING.equals(cacheKey)) { - computeScriptMD5(script); - CompiledScript compiledScript = getCompiledScript(scriptMd5, key -> { - try { - return ((Compilable) scriptEngine).compile(script); - } catch (ScriptException e) { - throw new ScriptCompilationInvocationTargetException(e); + if (supportsCompilable) { + if (!ScriptingBeanInfoSupport.FALSE_AS_STRING.equals(cacheChecked)) { + computeScriptCacheKey(script); + CompiledScript compiledScript = getCompiledScript(scriptCacheKey, key -> { + try { + return ((Compilable) scriptEngine).compile(script); + } catch (ScriptException e) { + if (logger.isDebugEnabled()) { + logger.debug("Cache missed access: failed compile of JSR223 element named: '{}'", getName()); + } + throw new ScriptCompilationInvocationTargetException(e); + } + }); + return compiledScript.eval(bindings); + } else { + computeScriptCacheKey(script.hashCode()); + //simulate a cache miss when JSR223 'Cache compiled script if available' is unchecked to have better view of cache usage + var unused = COMPILED_SCRIPT_CACHE.get(scriptCacheKey, k -> { + return null; + }); + if (logger.isDebugEnabled()) { + logger.debug("Cache missed access: 'Cache compile' is unchecked for JSR223 element named: '{}'", getName()); } - }); - return compiledScript.eval(bindings); + return scriptEngine.eval(script, bindings); + } } else { return scriptEngine.eval(script, bindings); } @@ -236,7 +253,7 @@ protected Object processFileOrScript(ScriptEngine scriptEngine, final Bindings p } } catch (ScriptException ex) { Throwable rootCause = ex.getCause(); - if(isStopCondition(rootCause)) { + if (isStopCondition(rootCause)) { throw (RuntimeException) ex.getCause(); } else { throw ex; @@ -274,7 +291,7 @@ private static CompiledScript getCompiledScript( * @throws ScriptException if compilation fails */ public boolean compile() - throws ScriptException, IOException { + throws ScriptException, IOException { String lang = getScriptLanguageWithDefault(); ScriptEngine scriptEngine = getInstance().getEngineByName(lang); boolean supportsCompilable = scriptEngine instanceof Compilable @@ -305,27 +322,46 @@ public boolean compile() } /** - * compute MD5 if it is null + * compute MD5 of a script if null */ - private void computeScriptMD5(String script) { + private void computeScriptCacheKey(String script) { // compute the md5 of the script if needed - if(scriptMd5 == null) { - scriptMd5 = ScriptCacheKey.ofString(DigestUtils.md5Hex(script)); + if (scriptCacheKey == null) { + scriptCacheKey = ScriptCacheKey.ofString(DigestUtils.md5Hex(script)); + } + } + + /** + * compute cache key for a file based script if null + */ + private void computeScriptCacheKey(File scriptFile) { + if (scriptCacheKey == null) { + scriptCacheKey = ScriptCacheKey.ofFile(getScriptLanguage(), scriptFile.getAbsolutePath(), scriptFile.lastModified()); } } /** - * @return the cacheKey + * compute cache key of a long value if null + */ + private void computeScriptCacheKey(int reference) { + if (scriptCacheKey == null) { + scriptCacheKey = ScriptCacheKey.ofString(Integer.toString(reference)); + } + } + + + /** + * @return the cacheChecked */ public String getCacheKey() { - return cacheKey; + return cacheChecked; } /** - * @param cacheKey the cacheKey to set + * @param cacheChecked the cacheChecked to set */ - public void setCacheKey(String cacheKey) { - this.cacheKey = cacheKey; + public void setCacheKey(String cacheChecked) { + this.cacheChecked = cacheChecked; } /** @@ -333,7 +369,7 @@ public void setCacheKey(String cacheKey) { */ @Override public void testStarted() { - // NOOP + testStarted(""); } /** @@ -341,7 +377,13 @@ public void testStarted() { */ @Override public void testStarted(String host) { - // NOOP + synchronized (lock) { + if (COMPILED_SCRIPT_CACHE == null) { + COMPILED_SCRIPT_CACHE = + Caffeine.from(JMeterUtils.getPropDefault("jsr223.compiled_scripts_cache_spec", "maximumSize=" + + JMeterUtils.getPropDefault("jsr223.compiled_scripts_cache_size", 100) + ",recordStats")).build(); + } + } } /** @@ -357,8 +399,25 @@ public void testEnded() { */ @Override public void testEnded(String host) { - COMPILED_SCRIPT_CACHE.invalidateAll(); - scriptMd5 = null; + synchronized (lock) { + if (COMPILED_SCRIPT_CACHE != null) { + CacheStats stats = COMPILED_SCRIPT_CACHE.stats(); + logger.info("JSR223 cached scripts: {}, requestsCount: {} (hitCount: {} + missedCount: {}), (hitRate: {}, missRate: {}), " + + "loadCount: {} (loadSuccessCount: {} + loadFailureCount: {}), " + + "evictionCount: {}, evictionWeight: {}, " + + "totalLoadTime: {} ms, averageLoadPenalty: {} ms", + COMPILED_SCRIPT_CACHE.estimatedSize(), + stats.requestCount(), stats.hitCount(), stats.missCount(), + String.format("%.02f", stats.hitRate()), String.format("%.02f", stats.missRate()), + stats.loadCount(), stats.loadSuccessCount(), stats.loadFailureCount(), + stats.evictionCount(), stats.evictionWeight(), + String.format("%.02f", (stats.totalLoadTime() / 100000f)), String.format("%.02f", (stats.averageLoadPenalty() / 100000f))); + COMPILED_SCRIPT_CACHE.invalidateAll(); + COMPILED_SCRIPT_CACHE.cleanUp(); + COMPILED_SCRIPT_CACHE = null; + } + } + scriptCacheKey = null; } public String getScriptLanguage() { diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/CacheManager.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/CacheManager.java index 6b1c1d48b15..a09c2b3690a 100644 --- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/CacheManager.java +++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/CacheManager.java @@ -283,7 +283,7 @@ private void setCache(String lastModified, String cacheControl, String expires, } else { // Makes expiresDate effectively-final Date entryExpiresDate = expiresDate; - getCache().get( + var unused = getCache().get( url, key -> { CacheEntry cacheEntry = new CacheEntry(lastModified, entryExpiresDate, etag, null); diff --git a/xdocs/usermanual/component_reference.xml b/xdocs/usermanual/component_reference.xml index 213a62758ed..dd27b725128 100644 --- a/xdocs/usermanual/component_reference.xml +++ b/xdocs/usermanual/component_reference.xml @@ -1185,7 +1185,7 @@ To benefit from this feature: Cache size is controlled by the following JMeter property (jmeter.properties): -jsr223.compiled_scripts_cache_size=100 + jsr223.compiled_scripts_cache_size=100 or via a more complex cache setup using the jsr223.compiled_scripts_cache_spec Unlike the , the interpreter is not saved between invocations. JSR223 Test Elements using Script file or Script text + checked Cache compiled script if available are now compiled if ScriptEngine supports this feature, this enables great performance enhancements. diff --git a/xdocs/usermanual/properties_reference.xml b/xdocs/usermanual/properties_reference.xml index d74ece0613d..663905f2dc3 100644 --- a/xdocs/usermanual/properties_reference.xml +++ b/xdocs/usermanual/properties_reference.xml @@ -1984,7 +1984,14 @@ JMETER-SERVER Used by JSR-223 elements.
Size of compiled scripts cache.
- Defaults to: 100
+ Defaults to: 100 + + + Used by JSR-223 elements.
+ Caffeine framework spec configuration in String format. Overrides jsr223.compiled_scripts_cache_size
+ Defaults to: maximumSize=jsr223.compiled_scripts_cache_size,recordStats. + Extra details: Caffeine spec +