From 0ef288bee4a1768149f555479622e8cac4946cd2 Mon Sep 17 00:00:00 2001 From: Roger Abelenda Date: Wed, 15 May 2024 12:59:35 -0300 Subject: [PATCH] Add showTimeline in TestPlan to ease reviewing thread groups configurations in test plans Now is not needed to move the thread group separate from the test plan to be able to invoke showTimeline, just invoke it in test plan and get timelines for all thread groups in the testplan --- .../jmeter/javadsl/core/DslTestPlan.java | 79 +++++++++++++++++++ .../core/testelements/BaseTestElement.java | 4 +- .../core/threadgroups/BaseThreadGroup.java | 12 +++ .../threadgroups/DslDefaultThreadGroup.java | 19 +++-- .../core/threadgroups/LoadTimeLine.java | 54 +++++++++++++ .../core/threadgroups/RpsThreadGroup.java | 26 ++++-- 6 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/LoadTimeLine.java 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); } }