diff --git a/checksum.xml b/checksum.xml
index e2b2a158488..ec423515669 100644
--- a/checksum.xml
+++ b/checksum.xml
@@ -32,6 +32,7 @@
+
diff --git a/dev/count-trans.sh b/dev/count-trans.sh
new file mode 100755
index 00000000000..ff9459d41ac
--- /dev/null
+++ b/dev/count-trans.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+file=$1
+shift
+
+for transaction in $@
+do
+ total=$(grep ",$transaction," $file |wc -l)
+ ok=$(grep ",$transaction," $file | grep ",true," |wc -l)
+ ko=$(grep ",$transaction," $file | grep ",false," |wc -l)
+ echo "$transaction Total: $total, OK: $ok, KO: $ko"
+done
diff --git a/dev/launch-all.sh b/dev/launch-all.sh
new file mode 100755
index 00000000000..52785cfa18c
--- /dev/null
+++ b/dev/launch-all.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+THREADS=${1:-500}
+shift
+
+echo "***NO DISRUPTOR***"
+./launch.sh $THREADS 60 300 0 IRRELEVANT $1
+
+echo "***WITH DISRUPTOR***"
+for SIZE in 1024 65536 131072 262144 524288
+do
+ for STRATEGY in BlockingWaitStrategy SleepingWaitStrategy YieldingWaitStrategy BusySpinWaitStrategy
+ do
+ ./launch.sh $THREADS 60 300 $SIZE $STRATEGY $1
+ done
+done
+
+echo "***STATS***"
+for dir in test-*-*
+do
+ echo "$dir: $(./count-trans.sh $dir/results.csv RJ-0)"
+done
+
+echo "***ERRORS***"
+for dir in test-*-*
+do
+ echo "$dir: $(grep -c 'ERROR\|xception' $dir/jmeter.log)"
+done
+
diff --git a/dev/launch.sh b/dev/launch.sh
new file mode 100755
index 00000000000..231510b1fd9
--- /dev/null
+++ b/dev/launch.sh
@@ -0,0 +1,38 @@
+THREADS=${1:-500}
+RAMPUP=${2:-60}
+DURATION=${3:-300}
+RING_BUFFER_SIZE=${4:-65536}
+STRATEGY=${5:-BlockingWaitStrategy}
+WITH_SUMMARIZER=$6
+
+REV=$(git rev-parse --short HEAD)
+if [ -z "$WITH_SUMMARIZER" ]
+then
+ SUMMARIZER_ARG="-Jsummariser.name="
+ TEST_OUTDIR="test-$REV-$THREADS-$RING_BUFFER_SIZE-$STRATEGY-$(date +%Y%m%d-%H%M%S)"
+else
+ TEST_OUTDIR="test-$REV-$THREADS-$RING_BUFFER_SIZE-$STRATEGY-$(date +%Y%m%d-%H%M%S)-WITH-SUMMMARIZER"
+fi
+
+HEAP="-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$TEST_OUTDIR"
+# Flight recorder
+HEAP="$HEAP -XX:StartFlightRecording=disk=true,delay=2m,duration=2m,dumponexit=true,filename=$TEST_OUTDIR/recording.jfr,maxsize=2g,maxage=1d,settings=profile,path-to-gc-roots=true"
+HEAP="$HEAP -Duser.timezone=Europe/Paris -Djava.awt.headless=true"
+HEAP="$HEAP -Dcom.sun.management.jmxremote.authenticate=false"
+HEAP="$HEAP -Dcom.sun.management.jmxremote.ssl=false -Xlog:gc*,gc+age=trace,gc+heap=debug:file=$TEST_OUTDIR/gc_jmeter.log"
+HEAP="$HEAP -Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false"
+#HEAP="$HEAP -verbose:gc"
+HEAP="$HEAP -XX:MaxMetaspaceSize=1g -XX:G1HeapRegionSize=32m -XX:MaxGCPauseMillis=50 -Xms5g -Xmx5g -Xss256k"
+HEAP="$HEAP -XX:ParallelGCThreads=10 -XX:ConcGCThreads=8"
+export HEAP
+
+mkdir $TEST_OUTDIR
+
+#-o $TEST_OUTDIR/report
+../bin/jmeter -Lcom.lmax.disruptor=DEBUG $SUMMARIZER_ARG \
+ -Jthreads=$THREADS \
+ -Jrampup=$RAMPUP \
+ -Jduration=$DURATION \
+ -Jjmeter.save.disruptor.ringbuffer.size=$RING_BUFFER_SIZE \
+ -Jjmeter.save.disruptor.wait-strategy=$STRATEGY \
+ -f -n -t 'test.jmx' -l $TEST_OUTDIR/results.csv -j $TEST_OUTDIR/jmeter.log
diff --git a/dev/stats.ods b/dev/stats.ods
new file mode 100644
index 00000000000..0dd550ab4f1
Binary files /dev/null and b/dev/stats.ods differ
diff --git a/dev/test.jmx b/dev/test.jmx
new file mode 100644
index 00000000000..969e7c05dfa
--- /dev/null
+++ b/dev/test.jmx
@@ -0,0 +1,49 @@
+
+
+
+
+
+ false
+ false
+
+
+
+
+
+
+
+ continue
+
+ false
+ -1
+
+ ${__P(threads, 500)}
+ ${__P(rampup, 60)}
+ true
+ ${__P(duration, 600)}
+
+ true
+
+
+
+
+
+
+ SleepTime
+ 0
+ =
+
+
+ SleepMask
+ 0x0
+ =
+
+
+
+ org.apache.jmeter.protocol.java.test.SleepTest
+
+
+
+
+
+
diff --git a/src/core/build.gradle.kts b/src/core/build.gradle.kts
index 0ea301f0396..8cbd82d8928 100644
--- a/src/core/build.gradle.kts
+++ b/src/core/build.gradle.kts
@@ -97,6 +97,7 @@ dependencies {
implementation("org.jodd:jodd-props")
implementation("org.mozilla:rhino")
implementation("org.slf4j:jcl-over-slf4j")
+ implementation("com.lmax:disruptor:3.4.2")
// TODO: JMeter bundles Xerces, however the reason is unknown
runtimeOnly("xerces:xercesImpl")
runtimeOnly("xml-apis:xml-apis")
diff --git a/src/core/src/main/java/org/apache/jmeter/reporters/ResultCollector.java b/src/core/src/main/java/org/apache/jmeter/reporters/ResultCollector.java
index 7326c87f8ae..26f7c8cc6ed 100644
--- a/src/core/src/main/java/org/apache/jmeter/reporters/ResultCollector.java
+++ b/src/core/src/main/java/org/apache/jmeter/reporters/ResultCollector.java
@@ -51,589 +51,683 @@
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.visualizers.Visualizer;
import org.apache.jorphan.util.JMeterError;
+import org.apache.logging.log4j.core.util.Integers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.lmax.disruptor.BlockingWaitStrategy;
+import com.lmax.disruptor.BusySpinWaitStrategy;
+import com.lmax.disruptor.RingBuffer;
+import com.lmax.disruptor.SleepingWaitStrategy;
+import com.lmax.disruptor.WaitStrategy;
+import com.lmax.disruptor.YieldingWaitStrategy;
+import com.lmax.disruptor.dsl.Disruptor;
+import com.lmax.disruptor.dsl.ProducerType;
+import com.lmax.disruptor.util.DaemonThreadFactory;
+
/**
- * This class handles all saving of samples.
- * The class must be thread-safe because it is shared between threads (NoThreadClone).
+ * This class handles all saving of samples. The class must be thread-safe
+ * because it is shared between threads (NoThreadClone).
*/
-public class ResultCollector extends AbstractListenerElement implements SampleListener, Clearable, Serializable,
- TestStateListener, Remoteable, NoThreadClone {
- /**
- * Keep track of the file writer and the configuration,
- * as the instance used to close them is not the same as the instance that creates
- * them. This means one cannot use the saved PrintWriter or use getSaveConfig()
- */
- private static class FileEntry{
- final PrintWriter pw;
- final SampleSaveConfiguration config;
- FileEntry(PrintWriter printWriter, SampleSaveConfiguration sampleSaveConfiguration){
- this.pw = printWriter;
- this.config = sampleSaveConfiguration;
- }
- }
-
- private static final class ShutdownHook implements Runnable {
-
- @Override
- public void run() {
- log.info("Shutdown hook started");
- synchronized (LOCK) {
- finalizeFileOutput();
- }
- log.info("Shutdown hook ended");
- }
- }
- private static final Logger log = LoggerFactory.getLogger(ResultCollector.class);
-
- private static final long serialVersionUID = 234L;
-
- // This string is used to identify local test runs, so must not be a valid host name
- private static final String TEST_IS_LOCAL = "*local*"; // $NON-NLS-1$
-
- private static final String TESTRESULTS_START = ""; // $NON-NLS-1$
-
- private static final String TESTRESULTS_START_V1_1_PREVER = ""; // $NON-NLS-1$
-
- private static final String TESTRESULTS_END = ""; // $NON-NLS-1$
-
- // we have to use version 1.0, see bug 59973
- private static final String XML_HEADER = ""; // $NON-NLS-1$
-
- private static final int MIN_XML_FILE_LEN = XML_HEADER.length() + TESTRESULTS_START.length()
- + TESTRESULTS_END.length();
-
- public static final String FILENAME = "filename"; // $NON-NLS-1$
-
- private static final String SAVE_CONFIG = "saveConfig"; // $NON-NLS-1$
-
- private static final String ERROR_LOGGING = "ResultCollector.error_logging"; // $NON-NLS-1$
-
- private static final String SUCCESS_ONLY_LOGGING = "ResultCollector.success_only_logging"; // $NON-NLS-1$
-
- /** AutoFlush on each line */
- private static final boolean SAVING_AUTOFLUSH = JMeterUtils.getPropDefault("jmeter.save.saveservice.autoflush", false); //$NON-NLS-1$
-
- // Static variables
-
- // Lock used to guard static mutable variables
- private static final Object LOCK = new Object();
-
- private static final Map files = new HashMap<>();
-
- /**
- * Shutdown Hook that ensures PrintWriter is flushed is CTRL+C or kill is called during a test
- */
- private static Thread shutdownHook;
-
- /**
- * The instance count is used to keep track of whether any tests are currently running.
- * It's not possible to use the constructor or threadStarted etc as tests may overlap
- * e.g. a remote test may be started,
- * and then a local test started whilst the remote test is still running.
- */
- private static int instanceCount; // Keep track of how many instances are active
-
- // Instance variables (guarded by volatile)
- private transient volatile PrintWriter out;
-
- /**
- * Is a test running ?
- */
- private volatile boolean inTest = false;
-
- private volatile boolean isStats = false;
-
- /** the summarizer to which this result collector will forward the samples */
- private volatile Summariser summariser;
-
- /**
- * No-arg constructor.
- */
- public ResultCollector() {
- this(null);
- }
-
- /**
- * Constructor which sets the used {@link Summariser}
- * @param summer The {@link Summariser} to use
- */
- public ResultCollector(Summariser summer) {
- setErrorLogging(false);
- setSuccessOnlyLogging(false);
- setProperty(new ObjectProperty(SAVE_CONFIG, new SampleSaveConfiguration()));
- summariser = summer;
- }
-
- // Ensure that the sample save config is not shared between copied nodes
- // N.B. clone only seems to be used for client-server tests
- @Override
- public Object clone(){
- ResultCollector clone = (ResultCollector) super.clone();
- clone.setSaveConfig((SampleSaveConfiguration)clone.getSaveConfig().clone());
- // Unfortunately AbstractTestElement does not call super.clone()
- clone.summariser = this.summariser;
- return clone;
- }
-
- private void setFilenameProperty(String f) {
- setProperty(FILENAME, f);
- }
-
- /**
- * Get the filename of the file this collector uses
- *
- * @return The name of the file
- */
- public String getFilename() {
- return getPropertyAsString(FILENAME);
- }
-
- /**
- * Get the state of error logging
- *
- * @return Flag whether errors should be logged
- */
- public boolean isErrorLogging() {
- return getPropertyAsBoolean(ERROR_LOGGING);
- }
-
- /**
- * Sets error logging flag
- *
- * @param errorLogging
- * The flag whether errors should be logged
- */
- public final void setErrorLogging(boolean errorLogging) {
- setProperty(new BooleanProperty(ERROR_LOGGING, errorLogging));
- }
-
- /**
- * Sets the flag whether only successful samples should be logged
- *
- * @param value
- * The flag whether only successful samples should be logged
- */
- public final void setSuccessOnlyLogging(boolean value) {
- if (value) {
- setProperty(new BooleanProperty(SUCCESS_ONLY_LOGGING, true));
- } else {
- removeProperty(SUCCESS_ONLY_LOGGING);
- }
- }
-
- /**
- * Get the state of successful only logging
- *
- * @return Flag whether only successful samples should be logged
- */
- public boolean isSuccessOnlyLogging() {
- return getPropertyAsBoolean(SUCCESS_ONLY_LOGGING,false);
- }
-
- /**
- * Decides whether or not to a sample is wanted based on:
- *
- * - errorOnly
- * - successOnly
- * - sample success
- *
- * Should only be called for single samples.
- *
- * @param success is sample successful
- * @return whether to log/display the sample
- */
- public boolean isSampleWanted(boolean success){
- boolean errorOnly = isErrorLogging();
- boolean successOnly = isSuccessOnlyLogging();
- return isSampleWanted(success, errorOnly, successOnly);
- }
-
- /**
- * Decides whether or not to a sample is wanted based on:
- *
- * - errorOnly
- * - successOnly
- * - sample success
- *
- * This version is intended to be called by code that loops over many samples;
- * it is cheaper than fetching the settings each time.
- * @param success status of sample
- * @param errorOnly if errors only wanted
- * @param successOnly if success only wanted
- * @return whether to log/display the sample
- */
- public static boolean isSampleWanted(boolean success, boolean errorOnly,
- boolean successOnly) {
- return (!errorOnly && !successOnly) ||
- (success && successOnly) ||
- (!success && errorOnly);
- // successOnly and errorOnly cannot both be set
- }
- /**
- * Sets the filename attribute of the ResultCollector object.
- *
- * @param f
- * the new filename value
- */
- public void setFilename(String f) {
- if (inTest) {
- return;
- }
- setFilenameProperty(f);
- }
-
- @Override
- public void testEnded(String host) {
- synchronized(LOCK){
- instanceCount--;
- if (instanceCount <= 0) {
- // No need for the hook now
- // Bug 57088 - prevent (im?)possible NPE
- if (shutdownHook != null) {
- Runtime.getRuntime().removeShutdownHook(shutdownHook);
- } else {
- log.warn("Should not happen: shutdownHook==null, instanceCount={}", instanceCount);
- }
- finalizeFileOutput();
- out = null;
- inTest = false;
- }
- }
-
- if(summariser != null) {
- summariser.testEnded(host);
- }
- }
-
- @Override
- public void testStarted(String host) {
- synchronized(LOCK){
- if (instanceCount == 0) { // Only add the hook once
- shutdownHook = new Thread(new ShutdownHook());
- Runtime.getRuntime().addShutdownHook(shutdownHook);
- }
- instanceCount++;
- try {
- if (out == null) {
- try {
- // Note: getFileWriter ignores a null filename
- out = getFileWriter(getFilename(), getSaveConfig());
- } catch (FileNotFoundException e) {
- out = null;
- }
- }
- if (getVisualizer() != null) {
- this.isStats = getVisualizer().isStats();
- }
- } catch (Exception e) {
- log.error("Exception occurred while initializing file output.", e);
- }
- }
- inTest = true;
-
- if(summariser != null) {
- summariser.testStarted(host);
- }
- }
-
- @Override
- public void testEnded() {
- testEnded(TEST_IS_LOCAL);
- }
-
- @Override
- public void testStarted() {
- testStarted(TEST_IS_LOCAL);
- }
-
- /**
- * Loads an existing sample data (JTL) file.
- * This can be one of:
- *
- * - XStream format
- * - CSV format
- *
- *
- */
- public void loadExistingFile() {
- final Visualizer visualizer = getVisualizer();
- if (visualizer == null) {
- return; // No point reading the file if there's no visualiser
- }
- boolean parsedOK = false;
- String filename = getFilename();
- File file = new File(filename);
- if (file.exists()) {
- try ( FileReader fr = new FileReader(file);
- BufferedReader dataReader = new BufferedReader(fr, 300)){
- // Get the first line, and see if it is XML
- String line = dataReader.readLine();
- dataReader.close();
- if (line == null) {
- log.warn("{} is empty", filename);
- } else {
- if (!line.startsWith(" 0) {
- writer.println(pi);
- }
- // Can't do it as a static initialisation, because SaveService
- // is being constructed when this is called
- writer.print(TESTRESULTS_START_V1_1_PREVER);
- writer.print(SaveService.getVERSION());
- writer.print(TESTRESULTS_START_V1_1_POSTVER);
- // Write the EOL separately so we generate LF line ends on Unix and Windows
- writer.print("\n"); // $NON-NLS-1$
- } else if (saveConfig.saveFieldNames()) {
- writer.println(CSVSaveService.printableFieldNamesToString(saveConfig));
- }
- }
-
- private static void writeFileEnd(PrintWriter pw, SampleSaveConfiguration saveConfig) {
- if (saveConfig.saveAsXml()) {
- pw.print("\n"); // $NON-NLS-1$
- pw.print(TESTRESULTS_END);
- pw.print("\n");// Added in version 1.1 // $NON-NLS-1$
- }
- }
-
- private static PrintWriter getFileWriter(final String pFilename, SampleSaveConfiguration saveConfig)
- throws IOException {
- if (pFilename == null || pFilename.length() == 0) {
- return null;
- }
- if(log.isDebugEnabled()) {
- log.debug("Getting file: {} in thread {}", pFilename, Thread.currentThread().getName());
- }
- String filename = FileServer.resolveBaseRelativeName(pFilename);
- filename = new File(filename).getCanonicalPath(); // try to ensure uniqueness (Bug 60822)
- FileEntry fe = files.get(filename);
- PrintWriter writer = null;
- boolean trimmed = true;
-
- if (fe == null) {
- if (saveConfig.saveAsXml()) {
- trimmed = trimLastLine(filename);
- } else {
- trimmed = new File(filename).exists();
- }
- // Find the name of the directory containing the file
- // and create it - if there is one
- File pdir = new File(filename).getParentFile();
- if (pdir != null) {
- // returns false if directory already exists, so need to check again
- if(pdir.mkdirs()){
- if (log.isInfoEnabled()) {
- log.info("Folder at {} was created", pdir.getAbsolutePath());
- }
- } // else if might have been created by another process so not a problem
- if (!pdir.exists()){
- log.warn("Error creating directories for {}", pdir);
- }
- }
- writer = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(filename,
- trimmed)), SaveService.getFileEncoding(StandardCharsets.UTF_8.name())), SAVING_AUTOFLUSH);
- if(log.isDebugEnabled()) {
- log.debug("Opened file: {} in thread {}", filename, Thread.currentThread().getName());
- }
- files.put(filename, new FileEntry(writer, saveConfig));
- } else {
- writer = fe.pw;
- }
- if (!trimmed) {
- log.debug("Writing header to file: {}", filename);
- writeFileStart(writer, saveConfig);
- }
- return writer;
- }
-
- // returns false if the file did not contain the terminator
- private static boolean trimLastLine(String filename) {
- try (RandomAccessFile raf = new RandomAccessFile(filename, "rw")){ // $NON-NLS-1$
- long len = raf.length();
- if (len < MIN_XML_FILE_LEN) {
- return false;
- }
- raf.seek(len - TESTRESULTS_END.length() - 10);
- String line;
- long pos = raf.getFilePointer();
- int end = 0;
- while ((line = raf.readLine()) != null)// reads to end of line OR end of file
- {
- end = line.indexOf(TESTRESULTS_END);
- if (end >= 0) // found the string
- {
- break;
- }
- pos = raf.getFilePointer();
- }
- if (line == null) {
- log.warn("Unexpected EOF trying to find XML end marker in {}", filename);
- return false;
- }
- raf.setLength(pos + end);// Truncate the file
- } catch (FileNotFoundException e) {
- return false;
- } catch (IOException e) {
- if (log.isWarnEnabled()) {
- log.warn("Error trying to find XML terminator. {}", e.toString());
- }
- return false;
- }
- return true;
- }
-
- @Override
- public void sampleStarted(SampleEvent e) {
- // NOOP
- }
-
- @Override
- public void sampleStopped(SampleEvent e) {
- // NOOP
- }
-
- /**
- * When a test result is received, display it and save it.
- *
- * @param event
- * the sample event that was received
- */
- @Override
- public void sampleOccurred(SampleEvent event) {
- SampleResult result = event.getResult();
-
- if (isSampleWanted(result.isSuccessful())) {
- sendToVisualizer(result);
- if (out != null && !isResultMarked(result) && !this.isStats) {
- SampleSaveConfiguration config = getSaveConfig();
- result.setSaveConfig(config);
- try {
- if (config.saveAsXml()) {
- SaveService.saveSampleResult(event, out);
- } else { // !saveAsXml
- CSVSaveService.saveSampleResult(event, out);
- }
- } catch (Exception err) {
- log.error("Error trying to record a sample", err); // should throw exception back to caller
- }
- }
- }
-
- if(summariser != null) {
- summariser.sampleOccurred(event);
- }
- }
-
- protected final void sendToVisualizer(SampleResult r) {
- if (getVisualizer() != null) {
- getVisualizer().add(r);
- }
- }
-
- /**
- * Checks if the sample result is marked or not, and marks it
- * @param res - the sample result to check
- * @return true if the result was marked
- */
- private boolean isResultMarked(SampleResult res) {
- String filename = getFilename();
- return res.markFile(filename);
- }
-
- /**
- * Flush PrintWriter to synchronize file contents
- */
- public void flushFile() {
- if (out != null) {
- log.info("forced flush through ResultCollector#flushFile");
- out.flush();
- }
- }
-
- private static void finalizeFileOutput() {
- for(Map.Entry me : files.entrySet()) {
- String key = me.getKey();
- ResultCollector.FileEntry value = me.getValue();
- try {
- log.debug("Closing: {}", key);
- writeFileEnd(value.pw, value.config);
- value.pw.close();
- if (value.pw.checkError()){
- log.warn("Problem detected during use of {}", key);
- }
- } catch(Exception ex) {
- log.error("Error closing file {}", key, ex);
- }
- }
- files.clear();
- }
-
- /**
- * @return Returns the saveConfig.
- */
- public SampleSaveConfiguration getSaveConfig() {
- try {
- return (SampleSaveConfiguration) getProperty(SAVE_CONFIG).getObjectValue();
- } catch (ClassCastException e) {
- setSaveConfig(new SampleSaveConfiguration());
- return getSaveConfig();
- }
- }
-
- /**
- * @param saveConfig
- * The saveConfig to set.
- */
- public void setSaveConfig(SampleSaveConfiguration saveConfig) {
- getProperty(SAVE_CONFIG).setObjectValue(saveConfig);
- }
-
- // This is required so that
- // @see org.apache.jmeter.gui.tree.JMeterTreeModel.getNodesOfType()
- // can find the Clearable nodes - the userObject has to implement the interface.
- @Override
- public void clearData() {
- // NOOP
- }
+public class ResultCollector extends AbstractListenerElement
+ implements SampleListener, Clearable, Serializable, TestStateListener, Remoteable, NoThreadClone {
+ /**
+ * Keep track of the file writer and the configuration, as the instance used to
+ * close them is not the same as the instance that creates them. This means one
+ * cannot use the saved PrintWriter or use getSaveConfig()
+ */
+ private static class FileEntry {
+ final PrintWriter pw;
+ final SampleSaveConfiguration config;
+
+ FileEntry(PrintWriter printWriter, SampleSaveConfiguration sampleSaveConfiguration) {
+ this.pw = printWriter;
+ this.config = sampleSaveConfiguration;
+ }
+ }
+
+ private static final class ShutdownHook implements Runnable {
+
+ @Override
+ public void run() {
+ log.info("Shutdown hook started");
+ synchronized (LOCK) {
+ finalizeFileOutput();
+ }
+ log.info("Shutdown hook ended");
+ }
+ }
+
+ private static final Logger log = LoggerFactory.getLogger(ResultCollector.class);
+
+ private static final long serialVersionUID = 234L;
+
+ // This string is used to identify local test runs, so must not be a valid host
+ // name
+ private static final String TEST_IS_LOCAL = "*local*"; // $NON-NLS-1$
+
+ private static final String TESTRESULTS_START = ""; // $NON-NLS-1$
+
+ private static final String TESTRESULTS_START_V1_1_PREVER = ""; // $NON-NLS-1$
+
+ private static final String TESTRESULTS_END = ""; // $NON-NLS-1$
+
+ // we have to use version 1.0, see bug 59973
+ private static final String XML_HEADER = ""; // $NON-NLS-1$
+
+ private static final int MIN_XML_FILE_LEN = XML_HEADER.length() + TESTRESULTS_START.length()
+ + TESTRESULTS_END.length();
+
+ public static final String FILENAME = "filename"; // $NON-NLS-1$
+
+ private static final String SAVE_CONFIG = "saveConfig"; // $NON-NLS-1$
+
+ private static final String ERROR_LOGGING = "ResultCollector.error_logging"; // $NON-NLS-1$
+
+ private static final String SUCCESS_ONLY_LOGGING = "ResultCollector.success_only_logging"; // $NON-NLS-1$
+
+ /** AutoFlush on each line */
+ private static final boolean SAVING_AUTOFLUSH = JMeterUtils.getPropDefault("jmeter.save.saveservice.autoflush", //$NON-NLS-1$
+ false);
+
+ private static final int DEFAULT_RING_BUFFER_SIZE = 0;
+ private static final int RING_BUFFER_SIZE = JMeterUtils.getPropDefault("jmeter.save.disruptor.ringbuffer.size",
+ DEFAULT_RING_BUFFER_SIZE);
+
+ private enum WaitingStrategy {
+ BlockingWaitStrategy, SleepingWaitStrategy, YieldingWaitStrategy, BusySpinWaitStrategy;
+ // LiteBlockingWaitStrategy experimental
+ // LiteTimeoutBlockingWaitStrategy undocumented
+ // PhasedBackoffWaitStrategy undocumented
+ // TimeoutBlockingWaitStrategy undocumented
+ }
+
+ private static final WaitingStrategy DEFAULT_WAIT_STRATEGY = WaitingStrategy.BlockingWaitStrategy;
+ private static final String WAIT_STRATEGY_STR = JMeterUtils.getPropDefault("jmeter.save.disruptor.wait-strategy",
+ DEFAULT_WAIT_STRATEGY.name());
+
+ // Static variables
+
+ // Lock used to guard static mutable variables
+ private static final Object LOCK = new Object();
+
+ private static final Map files = new HashMap<>();
+
+ /**
+ * Shutdown Hook that ensures PrintWriter is flushed is CTRL+C or kill is called
+ * during a test
+ */
+ private static Thread shutdownHook;
+
+ /**
+ * The instance count is used to keep track of whether any tests are currently
+ * running. It's not possible to use the constructor or threadStarted etc as
+ * tests may overlap e.g. a remote test may be started, and then a local test
+ * started whilst the remote test is still running.
+ */
+ private static int instanceCount; // Keep track of how many instances are active
+
+ // Instance variables (guarded by volatile)
+ private transient volatile PrintWriter out;
+
+ private transient volatile Disruptor disruptor;
+
+ private static class ResultCollectorEvent {
+ private SampleEvent sampleEvent;
+
+ public SampleEvent getSampleEvent() {
+ return sampleEvent;
+ }
+
+ public void setSampleEvent(SampleEvent sampleEvent) {
+ this.sampleEvent = sampleEvent;
+ }
+ }
+
+ /**
+ * Is a test running ?
+ */
+ private volatile boolean inTest = false;
+
+ private volatile boolean isStats = false;
+
+ /** the summarizer to which this result collector will forward the samples */
+ private volatile Summariser summariser;
+
+ /**
+ * No-arg constructor.
+ */
+ public ResultCollector() {
+ this(null);
+ }
+
+ /**
+ * Constructor which sets the used {@link Summariser}
+ *
+ * @param summer The {@link Summariser} to use
+ */
+ public ResultCollector(Summariser summer) {
+ setErrorLogging(false);
+ setSuccessOnlyLogging(false);
+ setProperty(new ObjectProperty(SAVE_CONFIG, new SampleSaveConfiguration()));
+ summariser = summer;
+ }
+
+ // Ensure that the sample save config is not shared between copied nodes
+ // N.B. clone only seems to be used for client-server tests
+ @Override
+ public Object clone() {
+ ResultCollector clone = (ResultCollector) super.clone();
+ clone.setSaveConfig((SampleSaveConfiguration) clone.getSaveConfig().clone());
+ // Unfortunately AbstractTestElement does not call super.clone()
+ clone.summariser = this.summariser;
+ return clone;
+ }
+
+ private void setFilenameProperty(String f) {
+ setProperty(FILENAME, f);
+ }
+
+ /**
+ * Get the filename of the file this collector uses
+ *
+ * @return The name of the file
+ */
+ public String getFilename() {
+ return getPropertyAsString(FILENAME);
+ }
+
+ /**
+ * Get the state of error logging
+ *
+ * @return Flag whether errors should be logged
+ */
+ public boolean isErrorLogging() {
+ return getPropertyAsBoolean(ERROR_LOGGING);
+ }
+
+ /**
+ * Sets error logging flag
+ *
+ * @param errorLogging The flag whether errors should be logged
+ */
+ public final void setErrorLogging(boolean errorLogging) {
+ setProperty(new BooleanProperty(ERROR_LOGGING, errorLogging));
+ }
+
+ /**
+ * Sets the flag whether only successful samples should be logged
+ *
+ * @param value The flag whether only successful samples should be logged
+ */
+ public final void setSuccessOnlyLogging(boolean value) {
+ if (value) {
+ setProperty(new BooleanProperty(SUCCESS_ONLY_LOGGING, true));
+ } else {
+ removeProperty(SUCCESS_ONLY_LOGGING);
+ }
+ }
+
+ /**
+ * Get the state of successful only logging
+ *
+ * @return Flag whether only successful samples should be logged
+ */
+ public boolean isSuccessOnlyLogging() {
+ return getPropertyAsBoolean(SUCCESS_ONLY_LOGGING, false);
+ }
+
+ /**
+ * Decides whether or not to a sample is wanted based on:
+ *
+ * - errorOnly
+ * - successOnly
+ * - sample success
+ *
+ * Should only be called for single samples.
+ *
+ * @param success is sample successful
+ * @return whether to log/display the sample
+ */
+ public boolean isSampleWanted(boolean success) {
+ boolean errorOnly = isErrorLogging();
+ boolean successOnly = isSuccessOnlyLogging();
+ return isSampleWanted(success, errorOnly, successOnly);
+ }
+
+ /**
+ * Decides whether or not to a sample is wanted based on:
+ *
+ * - errorOnly
+ * - successOnly
+ * - sample success
+ *
+ * This version is intended to be called by code that loops over many samples;
+ * it is cheaper than fetching the settings each time.
+ *
+ * @param success status of sample
+ * @param errorOnly if errors only wanted
+ * @param successOnly if success only wanted
+ * @return whether to log/display the sample
+ */
+ public static boolean isSampleWanted(boolean success, boolean errorOnly, boolean successOnly) {
+ return (!errorOnly && !successOnly) || (success && successOnly) || (!success && errorOnly);
+ // successOnly and errorOnly cannot both be set
+ }
+
+ /**
+ * Sets the filename attribute of the ResultCollector object.
+ *
+ * @param f the new filename value
+ */
+ public void setFilename(String f) {
+ if (inTest) {
+ return;
+ }
+ setFilenameProperty(f);
+ }
+
+ @Override
+ public void testEnded(String host) {
+ synchronized (LOCK) {
+
+ if (disruptor != null) {
+ log.info("Shutdown disruptor of ResultCollector '{}'", getName());
+ disruptor.shutdown();
+ log.info("Disruptor of ResultCollector '{}' stopped", getName());
+ }
+
+ instanceCount--;
+ if (instanceCount <= 0) {
+
+ // No need for the hook now
+ // Bug 57088 - prevent (im?)possible NPE
+ if (shutdownHook != null) {
+ Runtime.getRuntime().removeShutdownHook(shutdownHook);
+ } else {
+ log.warn("Should not happen: shutdownHook==null, instanceCount={}", instanceCount);
+ }
+ finalizeFileOutput();
+ out = null;
+ inTest = false;
+ }
+
+ }
+
+ if (summariser != null) {
+ summariser.testEnded(host);
+ }
+ }
+
+ private void saveSampleEvent(SampleEvent event, SampleSaveConfiguration saveConfig) {
+ try {
+ if (saveConfig.saveAsXml()) {
+ SaveService.saveSampleResult(event, out);
+ } else { // !saveAsXml
+ CSVSaveService.saveSampleResult(event, out);
+ }
+ } catch (Exception err) {
+ log.error("Error trying to record a sample", err); // should throw exception back to caller
+ }
+ }
+
+ private WaitStrategy buildWaitStrategy(WaitingStrategy strategy) {
+ WaitStrategy result;
+ switch (strategy) {
+ case BusySpinWaitStrategy:
+ result = new BusySpinWaitStrategy();
+ break;
+ case SleepingWaitStrategy:
+ result = new SleepingWaitStrategy();
+ break;
+ case YieldingWaitStrategy:
+ result = new YieldingWaitStrategy();
+ break;
+ default:
+ result = new BlockingWaitStrategy();
+ break;
+ }
+ return result;
+ }
+
+ @Override
+ public void testStarted(String host) {
+ synchronized (LOCK) {
+ if (instanceCount == 0) { // Only add the hook once
+ shutdownHook = new Thread(new ShutdownHook());
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+ }
+ instanceCount++;
+ try {
+ if (out == null) {
+ try {
+ // Note: getFileWriter ignores a null filename
+ out = getFileWriter(getFilename(), getSaveConfig());
+ } catch (FileNotFoundException e) {
+ out = null;
+ }
+ }
+ if (getVisualizer() != null) {
+ this.isStats = getVisualizer().isStats();
+ }
+ } catch (Exception e) {
+ log.error("Exception occurred while initializing file output.", e);
+ }
+
+ if (RING_BUFFER_SIZE > 0) {
+ int ringBufferSize = Integers.ceilingNextPowerOfTwo(RING_BUFFER_SIZE);
+ WaitingStrategy waitingStrategy = WaitingStrategy.valueOf(WAIT_STRATEGY_STR);
+ log.info("Initializing disruptor for result collector '{}' with ring buffer size: {} and wait strategy: {} ", getName(),
+ ringBufferSize, waitingStrategy);
+ disruptor = new Disruptor<>(ResultCollectorEvent::new, ringBufferSize, DaemonThreadFactory.INSTANCE,
+ ProducerType.MULTI, buildWaitStrategy(waitingStrategy));
+ disruptor.handleEventsWith((event, sequence, endOfBatch) -> {
+ saveSampleEvent(event.getSampleEvent(), getSaveConfig());
+ });
+ disruptor.start();
+ }
+
+ }
+ inTest = true;
+
+ if (summariser != null) {
+ summariser.testStarted(host);
+ }
+ }
+
+ @Override
+ public void testEnded() {
+ testEnded(TEST_IS_LOCAL);
+ }
+
+ @Override
+ public void testStarted() {
+ testStarted(TEST_IS_LOCAL);
+ }
+
+ /**
+ * Loads an existing sample data (JTL) file. This can be one of:
+ *
+ * - XStream format
+ * - CSV format
+ *
+ *
+ */
+ public void loadExistingFile() {
+ final Visualizer visualizer = getVisualizer();
+ if (visualizer == null) {
+ return; // No point reading the file if there's no visualiser
+ }
+ boolean parsedOK = false;
+ String filename = getFilename();
+ File file = new File(filename);
+ if (file.exists()) {
+ try (FileReader fr = new FileReader(file); BufferedReader dataReader = new BufferedReader(fr, 300)) {
+ // Get the first line, and see if it is XML
+ String line = dataReader.readLine();
+ dataReader.close();
+ if (line == null) {
+ log.warn("{} is empty", filename);
+ } else {
+ if (!line.startsWith(" 0) {
+ writer.println(pi);
+ }
+ // Can't do it as a static initialisation, because SaveService
+ // is being constructed when this is called
+ writer.print(TESTRESULTS_START_V1_1_PREVER);
+ writer.print(SaveService.getVERSION());
+ writer.print(TESTRESULTS_START_V1_1_POSTVER);
+ // Write the EOL separately so we generate LF line ends on Unix and Windows
+ writer.print("\n"); // $NON-NLS-1$
+ } else if (saveConfig.saveFieldNames()) {
+ writer.println(CSVSaveService.printableFieldNamesToString(saveConfig));
+ }
+ }
+
+ private static void writeFileEnd(PrintWriter pw, SampleSaveConfiguration saveConfig) {
+ if (saveConfig.saveAsXml()) {
+ pw.print("\n"); // $NON-NLS-1$
+ pw.print(TESTRESULTS_END);
+ pw.print("\n");// Added in version 1.1 // $NON-NLS-1$
+ }
+ }
+
+ private static PrintWriter getFileWriter(final String pFilename, SampleSaveConfiguration saveConfig)
+ throws IOException {
+ if (pFilename == null || pFilename.length() == 0) {
+ return null;
+ }
+ if (log.isDebugEnabled()) {
+ log.debug("Getting file: {} in thread {}", pFilename, Thread.currentThread().getName());
+ }
+ String filename = FileServer.resolveBaseRelativeName(pFilename);
+ filename = new File(filename).getCanonicalPath(); // try to ensure uniqueness (Bug 60822)
+ FileEntry fe = files.get(filename);
+ PrintWriter writer = null;
+ boolean trimmed = true;
+
+ if (fe == null) {
+ if (saveConfig.saveAsXml()) {
+ trimmed = trimLastLine(filename);
+ } else {
+ trimmed = new File(filename).exists();
+ }
+ // Find the name of the directory containing the file
+ // and create it - if there is one
+ File pdir = new File(filename).getParentFile();
+ if (pdir != null) {
+ // returns false if directory already exists, so need to check again
+ if (pdir.mkdirs()) {
+ if (log.isInfoEnabled()) {
+ log.info("Folder at {} was created", pdir.getAbsolutePath());
+ }
+ } // else if might have been created by another process so not a problem
+ if (!pdir.exists()) {
+ log.warn("Error creating directories for {}", pdir);
+ }
+ }
+ writer = new PrintWriter(
+ new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(filename, trimmed)),
+ SaveService.getFileEncoding(StandardCharsets.UTF_8.name())),
+ SAVING_AUTOFLUSH);
+ if (log.isDebugEnabled()) {
+ log.debug("Opened file: {} in thread {}", filename, Thread.currentThread().getName());
+ }
+ fe = new FileEntry(writer, saveConfig);
+ files.put(filename, fe);
+ } else {
+ writer = fe.pw;
+ }
+ if (!trimmed) {
+ log.debug("Writing header to file: {}", filename);
+ writeFileStart(writer, saveConfig);
+ }
+ return writer;
+ }
+
+ // returns false if the file did not contain the terminator
+ private static boolean trimLastLine(String filename) {
+ try (RandomAccessFile raf = new RandomAccessFile(filename, "rw")) { // $NON-NLS-1$
+ long len = raf.length();
+ if (len < MIN_XML_FILE_LEN) {
+ return false;
+ }
+ raf.seek(len - TESTRESULTS_END.length() - 10);
+ String line;
+ long pos = raf.getFilePointer();
+ int end = 0;
+ while ((line = raf.readLine()) != null)// reads to end of line OR end of file
+ {
+ end = line.indexOf(TESTRESULTS_END);
+ if (end >= 0) // found the string
+ {
+ break;
+ }
+ pos = raf.getFilePointer();
+ }
+ if (line == null) {
+ log.warn("Unexpected EOF trying to find XML end marker in {}", filename);
+ return false;
+ }
+ raf.setLength(pos + end);// Truncate the file
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ if (log.isWarnEnabled()) {
+ log.warn("Error trying to find XML terminator. {}", e.toString());
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void sampleStarted(SampleEvent e) {
+ // NOOP
+ }
+
+ @Override
+ public void sampleStopped(SampleEvent e) {
+ // NOOP
+ }
+
+ /**
+ * When a test result is received, display it and save it.
+ *
+ * @param event the sample event that was received
+ */
+ @Override
+ public void sampleOccurred(SampleEvent event) {
+ SampleResult result = event.getResult();
+ if (isSampleWanted(result.isSuccessful())) {
+ sendToVisualizer(result);
+ if (out != null && !isResultMarked(result) && !this.isStats) {
+ SampleSaveConfiguration config = getSaveConfig();
+ result.setSaveConfig(config);
+
+ if (disruptor != null) {
+ RingBuffer ringBuffer = disruptor.getRingBuffer();
+ ringBuffer.publishEvent((rbEvent, seq, sampleEvent) -> {
+ rbEvent.setSampleEvent(sampleEvent);
+ }, event);
+ } else {
+ saveSampleEvent(event, config);
+ }
+
+ }
+ }
+
+ if (summariser != null) {
+ summariser.sampleOccurred(event);
+ }
+ }
+
+ protected final void sendToVisualizer(SampleResult r) {
+ if (getVisualizer() != null) {
+ getVisualizer().add(r);
+ }
+ }
+
+ /**
+ * Checks if the sample result is marked or not, and marks it
+ *
+ * @param res - the sample result to check
+ * @return true if the result was marked
+ */
+ private boolean isResultMarked(SampleResult res) {
+ String filename = getFilename();
+ return res.markFile(filename);
+ }
+
+ /**
+ * Flush PrintWriter to synchronize file contents
+ */
+ public void flushFile() {
+ if (out != null) {
+ log.info("forced flush through ResultCollector#flushFile");
+ out.flush();
+ }
+ }
+
+ private static void finalizeFileOutput() {
+ for (Map.Entry me : files.entrySet()) {
+ String key = me.getKey();
+ ResultCollector.FileEntry value = me.getValue();
+ try {
+ log.debug("Closing: {}", key);
+ writeFileEnd(value.pw, value.config);
+ value.pw.close();
+ if (value.pw.checkError()) {
+ log.warn("Problem detected during use of {}", key);
+ }
+ } catch (Exception ex) {
+ log.error("Error closing file {}", key, ex);
+ }
+ }
+ files.clear();
+ }
+
+ /**
+ * @return Returns the saveConfig.
+ */
+ public SampleSaveConfiguration getSaveConfig() {
+ try {
+ return (SampleSaveConfiguration) getProperty(SAVE_CONFIG).getObjectValue();
+ } catch (ClassCastException e) {
+ setSaveConfig(new SampleSaveConfiguration());
+ return getSaveConfig();
+ }
+ }
+
+ /**
+ * @param saveConfig The saveConfig to set.
+ */
+ public void setSaveConfig(SampleSaveConfiguration saveConfig) {
+ getProperty(SAVE_CONFIG).setObjectValue(saveConfig);
+ }
+
+ // This is required so that
+ // @see org.apache.jmeter.gui.tree.JMeterTreeModel.getNodesOfType()
+ // can find the Clearable nodes - the userObject has to implement the interface.
+ @Override
+ public void clearData() {
+ // NOOP
+ }
}