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); } }