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 + } }