diff --git a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/DslTestPlan.java b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/DslTestPlan.java
index a62b482b..5e8fbbbc 100644
--- a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/DslTestPlan.java
+++ b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/DslTestPlan.java
@@ -1,5 +1,8 @@
package us.abstracta.jmeter.javadsl.core;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Font;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -10,7 +13,14 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
+import javax.swing.border.TitledBorder;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.control.gui.TestPlanGui;
import org.apache.jmeter.exceptions.IllegalUserActionException;
@@ -29,7 +39,9 @@
import us.abstracta.jmeter.javadsl.core.engines.JmeterEnvironment;
import us.abstracta.jmeter.javadsl.core.engines.JmeterGui;
import us.abstracta.jmeter.javadsl.core.testelements.TestElementContainer;
+import us.abstracta.jmeter.javadsl.core.threadgroups.BaseThreadGroup;
import us.abstracta.jmeter.javadsl.core.threadgroups.DslDefaultThreadGroup;
+import us.abstracta.jmeter.javadsl.core.threadgroups.LoadTimeLine;
/**
* Represents a JMeter test plan, with associated thread groups and other children elements.
@@ -170,6 +182,73 @@ and avoid NPE while updating RSyntaxTextArea in test plan load (e.g.: when test
}
}
+ /**
+ * For each thread group shows a graph with a timeline of planned load (threads or rps) to be
+ * generated.
+ *
+ * Graphs will be displayed in a popup window.
+ *
+ * This method eases test plan design when working with complex thread group profiles (several
+ * stages with ramps and holds).
+ *
+ * @since 1.28
+ */
+ public void showTimeline() {
+ List timeLines = buildThreadGroupTimeLines();
+ normalizeTimelines(timeLines);
+ JPanel panel = buildChartsContainerPanel();
+ int chartWidth = 800;
+ int chartHeight = timeLines.size() > 2 ? 200 : 300;
+ timeLines.forEach(tl -> panel.add(buildChart(tl, chartWidth, chartHeight)));
+ showAndWaitFrameWith(buildScrollPane(panel), "Load timeline", chartWidth + 20,
+ (chartHeight) * Math.min(3, timeLines.size()));
+ }
+
+ private List buildThreadGroupTimeLines() {
+ return children.stream()
+ .filter(c -> c instanceof BaseThreadGroup)
+ .map(c -> ((BaseThreadGroup>) c).buildLoadTimeline())
+ .collect(Collectors.toList());
+ }
+
+ private void normalizeTimelines(List timeLines) {
+ long maxTime = timeLines.stream()
+ .mapToLong(LoadTimeLine::getMaxTime)
+ .max()
+ .orElse(0L);
+ timeLines.forEach(tl -> {
+ tl.add(1, 0);
+ long chartMaxTime = tl.getMaxTime();
+ if (chartMaxTime < maxTime) {
+ tl.add(maxTime - chartMaxTime + 1, 0);
+ }
+ });
+ }
+
+ private JPanel buildChartsContainerPanel() {
+ JPanel ret = new JPanel();
+ ret.setLayout(new BoxLayout(ret, BoxLayout.Y_AXIS));
+ return ret;
+ }
+
+ private JComponent buildChart(LoadTimeLine timeLine, int width, int height) {
+ JComponent ret = timeLine.buildChart();
+ TitledBorder border = BorderFactory.createTitledBorder(timeLine.getName());
+ border.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+ border.setTitleFont(new Font("Arial", Font.BOLD, 14));
+ border.setTitleJustification(TitledBorder.CENTER);
+ ret.setBorder(border);
+ ret.setPreferredSize(new Dimension(width, height));
+ return ret;
+ }
+
+ private JScrollPane buildScrollPane(Component component) {
+ JScrollPane ret = new JScrollPane(component);
+ ret.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+ ret.getVerticalScrollBar().setUnitIncrement(8); // makes scroll faster
+ return ret;
+ }
+
/**
* Saves the given test plan as JMX, which allows it to be loaded in JMeter GUI.
*
diff --git a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/testelements/BaseTestElement.java b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/testelements/BaseTestElement.java
index 1a2a581d..811dd780 100644
--- a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/testelements/BaseTestElement.java
+++ b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/testelements/BaseTestElement.java
@@ -1,6 +1,7 @@
package us.abstracta.jmeter.javadsl.core.testelements;
import java.awt.Component;
+import java.awt.Dimension;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.BeanInfo;
@@ -138,6 +139,7 @@ public void showTestElementGui(Component guiComponent, Runnable closeListener) {
protected void showFrameWith(Component content, String title, int width, int height,
Runnable closeListener) {
+ content.setPreferredSize(new Dimension(width, height));
JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(
closeListener != null ? WindowConstants.DISPOSE_ON_CLOSE : WindowConstants.EXIT_ON_CLOSE);
@@ -151,8 +153,8 @@ public void windowClosed(WindowEvent e) {
});
}
frame.setLocation(200, 200);
- frame.setSize(width, height);
frame.add(content);
+ frame.pack();
frame.setVisible(true);
}
diff --git a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/BaseThreadGroup.java b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/BaseThreadGroup.java
index 05756e56..93d619f9 100644
--- a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/BaseThreadGroup.java
+++ b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/BaseThreadGroup.java
@@ -7,6 +7,7 @@
import org.apache.jmeter.threads.AbstractThreadGroup;
import us.abstracta.jmeter.javadsl.codegeneration.params.EnumParam.EnumPropertyValue;
import us.abstracta.jmeter.javadsl.core.DslTestElement;
+import us.abstracta.jmeter.javadsl.core.DslTestPlan;
import us.abstracta.jmeter.javadsl.core.testelements.TestElementContainer;
import us.abstracta.jmeter.javadsl.core.threadgroups.BaseThreadGroup.ThreadGroupChild;
@@ -61,6 +62,17 @@ protected TestElement buildTestElement() {
protected abstract AbstractThreadGroup buildThreadGroup();
+ /**
+ * This method is used by {@link DslTestPlan#showTimeline()} to get the timeline chart for
+ * this thread group.
+ *
+ * @return the timeline chart for this thread group or null if it is not supported.
+ * @since 1.28
+ */
+ public LoadTimeLine buildLoadTimeline() {
+ return null;
+ }
+
/**
* Test elements that can be added as direct children of a thread group in jmeter should implement
* this interface.
diff --git a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/DslDefaultThreadGroup.java b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/DslDefaultThreadGroup.java
index 902c6aa3..c6367cc0 100644
--- a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/DslDefaultThreadGroup.java
+++ b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/DslDefaultThreadGroup.java
@@ -18,7 +18,6 @@
import us.abstracta.jmeter.javadsl.core.threadgroups.defaultthreadgroup.SimpleThreadGroupHelper;
import us.abstracta.jmeter.javadsl.core.threadgroups.defaultthreadgroup.Stage;
import us.abstracta.jmeter.javadsl.core.threadgroups.defaultthreadgroup.UltimateThreadGroupHelper;
-import us.abstracta.jmeter.javadsl.core.util.SingleSeriesTimelinePanel;
/**
* Represents the standard thread group test element included by JMeter.
@@ -398,14 +397,24 @@ public AbstractThreadGroup buildThreadGroup() {
* @since 0.26
*/
public void showTimeline() {
+ showAndWaitFrameWith(buildLoadTimeline().buildChart(), name + " threads timeline", 800, 300);
+ }
+
+ @Override
+ public LoadTimeLine buildLoadTimeline() {
if (stages.stream().anyMatch(s -> !s.isFixedStage())) {
throw new IllegalStateException(
"Can't display timeline when some JMeter expression is used in any ramp or hold.");
+ } else if (stages.size() == 1 && stages.get(0).iterations() != null
+ || stages.size() == 2 && stages.get(1).iterations() != null
+ || stages.size() == 3 && stages.get(2).iterations() != null) {
+ throw new IllegalStateException(
+ "Can't display timeline when thread group is configured with iterations.");
}
- SingleSeriesTimelinePanel chart = new SingleSeriesTimelinePanel("Threads");
- chart.add(0, 0);
- stages.forEach(s -> chart.add(((Duration) s.duration()).toMillis(), (int) s.threadCount()));
- showAndWaitFrameWith(chart, name + " threads timeline", 800, 300);
+ LoadTimeLine ret = new LoadTimeLine(name, "Threads");
+ ret.add(0, 0);
+ stages.forEach(s -> ret.add(((Duration) s.duration()).toMillis(), (int) s.threadCount()));
+ return ret;
}
public static class CodeBuilder extends MethodCallBuilder {
diff --git a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/LoadTimeLine.java b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/LoadTimeLine.java
new file mode 100644
index 00000000..7bc1370a
--- /dev/null
+++ b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/LoadTimeLine.java
@@ -0,0 +1,54 @@
+package us.abstracta.jmeter.javadsl.core.threadgroups;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.swing.JComponent;
+import us.abstracta.jmeter.javadsl.core.util.SingleSeriesTimelinePanel;
+
+public class LoadTimeLine {
+
+ private final String name;
+ private final String loadUnit;
+ private final List timePoints = new ArrayList<>();
+
+ public LoadTimeLine(String name, String loadUnit) {
+ this.name = name;
+ this.loadUnit = loadUnit;
+ }
+
+ public void add(long timeMillis, double value) {
+ timePoints.add(new TimePoint(timeMillis, value));
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public JComponent buildChart() {
+ SingleSeriesTimelinePanel ret = new SingleSeriesTimelinePanel(loadUnit);
+ for (TimePoint tp : timePoints) {
+ ret.add(tp.timeMillis, tp.value);
+ }
+ return ret;
+ }
+
+ public long getMaxTime() {
+ return timePoints.stream()
+ .mapToLong(tp -> tp.timeMillis)
+ .max()
+ .orElse(0L);
+ }
+
+ private static class TimePoint {
+
+ private final long timeMillis;
+ private final double value;
+
+ private TimePoint(long timeIncrMillis, double value) {
+ this.timeMillis = timeIncrMillis;
+ this.value = value;
+ }
+
+ }
+
+}
diff --git a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/RpsThreadGroup.java b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/RpsThreadGroup.java
index 06fb8567..07f78198 100644
--- a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/RpsThreadGroup.java
+++ b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/RpsThreadGroup.java
@@ -19,7 +19,6 @@
import org.apache.jorphan.collections.HashTree;
import us.abstracta.jmeter.javadsl.core.BuildTreeContext;
import us.abstracta.jmeter.javadsl.core.util.JmeterFunction;
-import us.abstracta.jmeter.javadsl.core.util.SingleSeriesTimelinePanel;
/**
* Configures a thread group which dynamically adapts the number of threads and pauses to match a
@@ -286,13 +285,28 @@ protected AbstractThreadGroup buildThreadGroup() {
return ret;
}
- public void showTimeline() {
- SingleSeriesTimelinePanel chart = new SingleSeriesTimelinePanel(counting.label + " per second");
+ @Override
+ public LoadTimeLine buildLoadTimeline() {
+ LoadTimeLine ret = new LoadTimeLine(name, counting.label + " per second");
if (!schedules.isEmpty()) {
- chart.add(0, schedules.get(0).fromRps);
- schedules.forEach(s -> chart.add(s.durationSecs * 1000, s.toRps));
+ ret.add(0, schedules.get(0).fromRps);
+ schedules.forEach(s -> ret.add(s.durationSecs * 1000, s.toRps));
}
- showAndWaitFrameWith(chart, name + " timeline", 800, 300);
+ return ret;
+ }
+
+ /**
+ * Shows a graph with a timeline of planned rps count execution for this test plan.
+ *
+ * The graph will be displayed in a popup window.
+ *
+ * This method is provided mainly to ease test plan designing when working with complex thread
+ * group profiles (several stages with ramps and holds).
+ *
+ * @since 0.26
+ */
+ public void showTimeline() {
+ showAndWaitFrameWith(buildLoadTimeline().buildChart(), name + " timeline", 800, 300);
}
}