diff --git a/src/core/src/main/java/org/apache/jmeter/threads/VirtualThreadGroup.java b/src/core/src/main/java/org/apache/jmeter/threads/VirtualThreadGroup.java new file mode 100644 index 00000000000..cbb1e0c5350 --- /dev/null +++ b/src/core/src/main/java/org/apache/jmeter/threads/VirtualThreadGroup.java @@ -0,0 +1,160 @@ +package org.apache.jmeter.threads; + +import java.util.concurrent.ConcurrentHashMap; +import org.apache.jmeter.engine.StandardJMeterEngine; +import org.apache.jmeter.gui.GUIMenuSortOrder; +import org.apache.jorphan.collections.ListedHashTree; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@GUIMenuSortOrder(2) +public class VirtualThreadGroup extends AbstractThreadGroup { + private static final long serialVersionUID = 284L; + private static final Logger log = LoggerFactory.getLogger(VirtualThreadGroup.class); + + public static final String RAMP_TIME = "ThreadGroup.ramp_time"; + + private final ConcurrentHashMap allVirtualThreads = new ConcurrentHashMap<>(); + private volatile boolean running = false; + private int groupNumber; + + public VirtualThreadGroup() { + super(); + } + + public void setRampUp(int rampUp) { + setProperty(RAMP_TIME, rampUp); + } + + public int getRampUp() { + return getPropertyAsInt(RAMP_TIME, 1); + } + + @Override + public void start(int groupNum, ListenerNotifier notifier, ListedHashTree threadGroupTree, StandardJMeterEngine engine) { + this.running = true; + this.groupNumber = groupNum; + + int numThreads = getNumThreads(); + log.info("Starting VIRTUAL thread group... number={} threads={}", groupNumber, numThreads); + + JMeterVariables variables = JMeterContextService.getContext().getVariables(); + + for (int threadNum = 0; running && threadNum < numThreads; threadNum++) { + createVirtualThread(notifier, threadGroupTree, engine, threadNum, variables); + } + + log.info("Started virtual thread group number {}", groupNumber); + } + + private void createVirtualThread(ListenerNotifier notifier, ListedHashTree threadGroupTree, + StandardJMeterEngine engine, int threadNum, JMeterVariables variables) { + + JMeterThread jmThread = makeThread(engine, this, notifier, groupNumber, threadNum, + cloneTree(threadGroupTree), variables); + String threadName = getName() + " " + groupNumber + "-" + (threadNum + 1); + jmThread.setThreadName(threadName); + + Thread virtualThread; + try { + Class builderClass = Class.forName("java.lang.Thread$Builder$OfVirtual"); + Object builder = Thread.class.getMethod("ofVirtual").invoke(null); + builder = builderClass.getMethod("name", String.class).invoke(builder, threadName); + virtualThread = (Thread) builderClass.getMethod("start", Runnable.class).invoke(builder, jmThread); + log.debug("Created virtual thread: {}", threadName); + } catch (Exception e) { + log.warn("Virtual Threads not available, using platform thread for: {}", threadName); + virtualThread = new Thread(jmThread, threadName); + virtualThread.start(); + } + + allVirtualThreads.put(jmThread, virtualThread); + } + + @Override + public void threadFinished(JMeterThread thread) { + if (log.isDebugEnabled()) { + log.debug("Ending virtual thread {}", thread.getThreadName()); + } + allVirtualThreads.remove(thread); + } + + @Override + public void tellThreadsToStop() { + running = false; + allVirtualThreads.forEach((jmeterThread, thread) -> { + jmeterThread.stop(); + jmeterThread.interrupt(); + if (thread != null) { + thread.interrupt(); + } + }); + } + + @Override + public void stop() { + running = false; + allVirtualThreads.keySet().forEach(JMeterThread::stop); + } + + @Override + public int numberOfActiveThreads() { + return allVirtualThreads.size(); + } + + @Override + public boolean verifyThreadsStopped() { + return allVirtualThreads.values().stream().allMatch(thread -> !thread.isAlive()); + } + + @Override + public void waitThreadsStopped() { + allVirtualThreads.values().forEach(thread -> { + if (thread != null && thread.isAlive()) { + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + } + + @Override + public boolean stopThread(String threadName, boolean now) { + for (var entry : allVirtualThreads.entrySet()) { + JMeterThread jmeterThread = entry.getKey(); + if (jmeterThread.getThreadName().equals(threadName)) { + jmeterThread.stop(); + if (now && entry.getValue() != null) { + entry.getValue().interrupt(); + } + return true; + } + } + return false; + } + + @Override + public JMeterThread addNewThread(int delay, StandardJMeterEngine engine) { + JMeterContext context = JMeterContextService.getContext(); + int numThreads; + + synchronized (this) { + numThreads = getNumThreads(); + setNumThreads(numThreads + 1); + } + + JMeterVariables variables = context.getVariables(); + createVirtualThread(null, null, engine, numThreads, variables); + + JMeterThread newJmThread = allVirtualThreads.keySet().stream() + .filter(t -> t.getThreadName().contains(String.valueOf(numThreads))) + .findFirst() + .orElse(null); + + JMeterContextService.addTotalThreads(1); + log.info("Started new virtual thread in group {}", groupNumber); + return newJmThread; + } +} diff --git a/src/core/src/main/java/org/apache/jmeter/threads/gui/VirtualThreadGroupGui.java b/src/core/src/main/java/org/apache/jmeter/threads/gui/VirtualThreadGroupGui.java new file mode 100644 index 00000000000..5eb5fb6dab1 --- /dev/null +++ b/src/core/src/main/java/org/apache/jmeter/threads/gui/VirtualThreadGroupGui.java @@ -0,0 +1,112 @@ +package org.apache.jmeter.threads.gui; + +import java.awt.BorderLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import org.apache.jmeter.control.LoopController; +import org.apache.jmeter.control.gui.LoopControlPanel; +import org.apache.jmeter.gui.util.VerticalPanel; +import org.apache.jmeter.threads.VirtualThreadGroup; +import org.apache.jmeter.testelement.TestElement; + +public class VirtualThreadGroupGui extends AbstractThreadGroupGui { + + private static final long serialVersionUID = 285L; + + private JTextField numThreadsField; + private JTextField rampUpField; + private LoopControlPanel loopPanel; + + public VirtualThreadGroupGui() { + super(); + init(); + } + + private void init() { + setLayout(new BorderLayout(0, 5)); + setBorder(makeBorder()); + add(makeTitlePanel(), BorderLayout.NORTH); + + JPanel mainPanel = new VerticalPanel(); + + // Number of Threads + JPanel numThreadsPanel = new JPanel(new BorderLayout(5, 0)); + JLabel numThreadsLabel = new JLabel("Number of Virtual Threads:"); + numThreadsField = new JTextField("1", 6); + numThreadsPanel.add(numThreadsLabel, BorderLayout.WEST); + numThreadsPanel.add(numThreadsField, BorderLayout.CENTER); + + // Ramp-Up Period + JPanel rampUpPanel = new JPanel(new BorderLayout(5, 0)); + JLabel rampUpLabel = new JLabel("Ramp-Up Period (seconds):"); + rampUpField = new JTextField("1", 6); + rampUpPanel.add(rampUpLabel, BorderLayout.WEST); + rampUpPanel.add(rampUpField, BorderLayout.CENTER); + + // Loop Controller Panel + loopPanel = new LoopControlPanel(); + + mainPanel.add(numThreadsPanel); + mainPanel.add(rampUpPanel); + mainPanel.add(loopPanel); + + add(mainPanel, BorderLayout.CENTER); + } + + @Override + public String getLabelResource() { + return "virtual_thread_group"; + } + + @Override + public TestElement createTestElement() { + VirtualThreadGroup tg = new VirtualThreadGroup(); + modifyTestElement(tg); + return tg; + } + + @Override + public void modifyTestElement(TestElement element) { + super.modifyTestElement(element); + if (element instanceof VirtualThreadGroup) { + VirtualThreadGroup vtg = (VirtualThreadGroup) element; + + // Set basic properties + try { + vtg.setNumThreads(Integer.parseInt(numThreadsField.getText())); + vtg.setRampUp(Integer.parseInt(rampUpField.getText())); + } catch (NumberFormatException e) { + vtg.setNumThreads(1); + vtg.setRampUp(1); + } + + // CRITICAL: Set the main controller + LoopController controller = (LoopController) loopPanel.createTestElement(); + vtg.setSamplerController(controller); + } + } + + @Override + public void configure(TestElement element) { + super.configure(element); + if (element instanceof VirtualThreadGroup) { + VirtualThreadGroup vtg = (VirtualThreadGroup) element; + numThreadsField.setText(String.valueOf(vtg.getNumThreads())); + rampUpField.setText(String.valueOf(vtg.getRampUp())); + + // Configure loop controller + if (vtg.getSamplerController() != null) { + loopPanel.configure(vtg.getSamplerController()); + } + } + } + + @Override + public void clearGui() { + super.clearGui(); + numThreadsField.setText("1"); + rampUpField.setText("1"); + loopPanel.clearGui(); + } +} diff --git a/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties b/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties index 23721c4b4c9..f827394805b 100644 --- a/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties +++ b/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties @@ -1545,3 +1545,4 @@ xpath2_extractor_match_number_failure=MatchNumber out of bonds \: you_must_enter_a_valid_number=You must enter a valid number zh_cn=Chinese (Simplified) zh_tw=Chinese (Traditional) +virtual_thread_group=Virtual Thread Group