Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<JMeterThread, Thread> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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