From e3be4c63ecb2fb71554728349e3ebbed4ba4d7d8 Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:12:38 +0200 Subject: [PATCH 01/10] WIP: virtual threads --- .../com/xceptance/xlt/agent/LoadTest.java | 2 +- .../xceptance/xlt/agent/LoadTestRunner.java | 35 ++++-- .../xceptance/xlt/engine/RequestQueue.java | 4 +- .../com/xceptance/xlt/engine/SessionImpl.java | 59 +++++----- .../xlt/engine/XltThreadFactory.java | 102 ++++++++++++++++++ 5 files changed, 157 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java diff --git a/src/main/java/com/xceptance/xlt/agent/LoadTest.java b/src/main/java/com/xceptance/xlt/agent/LoadTest.java index aa956cc2a..ba49a5f97 100644 --- a/src/main/java/com/xceptance/xlt/agent/LoadTest.java +++ b/src/main/java/com/xceptance/xlt/agent/LoadTest.java @@ -86,7 +86,7 @@ public void run() // create runner for configuration final LoadTestRunner runner = new LoadTestRunner(config, agentInfo, timer); - runner.setDaemon(true); + //runner.setDaemon(true); // add runner to list of known runners testRunners.add(runner); diff --git a/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java b/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java index 1a84060e5..36eb227a3 100644 --- a/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java +++ b/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java @@ -30,6 +30,7 @@ import com.xceptance.xlt.api.engine.GlobalClock; import com.xceptance.xlt.engine.DataManagerImpl; import com.xceptance.xlt.engine.SessionImpl; +import com.xceptance.xlt.engine.XltThreadFactory; import com.xceptance.xlt.engine.util.TimerUtils; /** @@ -40,13 +41,18 @@ * * @author Jörg Werner (Xceptance Software Technologies GmbH) */ -public class LoadTestRunner extends Thread +public class LoadTestRunner { /** * Class logger instance. */ private static final Logger log = LoggerFactory.getLogger(LoadTestRunner.class); + /** + * Class logger instance. + */ + private static final XltThreadFactory XLT_THREAD_FACTORY = new XltThreadFactory(false, null); + /** * Configuration. */ @@ -72,6 +78,11 @@ public class LoadTestRunner extends Thread */ private volatile boolean aborted; + /** + * The main thread of this user runner. + */ + private final Thread thread; + /** * Creates a new LoadTestRunner object for the given load test configuration. Typically, multiple runners are * started for one test case configuration, so the number of the current runner is passed as well. @@ -85,16 +96,25 @@ public class LoadTestRunner extends Thread */ public LoadTestRunner(final TestUserConfiguration config, final AgentInfo agentInfo, final AbstractExecutionTimer timer) { - // create a new thread group for each LoadTestRunner as a means - // to keep the main thread and any supporting threads together - super(new ThreadGroup(config.getUserId()), config.getUserId()); - this.config = config; this.agentInfo = agentInfo; this.timer = timer; status = new TestUserStatus(); status.setUserName(config.getUserId()); + + thread = XLT_THREAD_FACTORY.newThread(this::run); + thread.setName(config.getUserId()); + } + + public void start() + { + thread.start(); + } + + public void join() throws InterruptedException + { + thread.join(); } /** @@ -110,8 +130,7 @@ public TestUserStatus getTestUserStatus() /** * Runs the test case as configured in the test case configuration. */ - @Override - public void run() + private void run() { try { @@ -196,7 +215,7 @@ public void run() } catch (final Exception ex) { - log.error("Failed to run test as user: " + getName(), ex); + log.error("Failed to run test as user: " + thread.getName(), ex); status.setState(TestUserStatus.State.Failed); status.setException(ex); diff --git a/src/main/java/com/xceptance/xlt/engine/RequestQueue.java b/src/main/java/com/xceptance/xlt/engine/RequestQueue.java index 9466cb807..305749b22 100644 --- a/src/main/java/com/xceptance/xlt/engine/RequestQueue.java +++ b/src/main/java/com/xceptance/xlt/engine/RequestQueue.java @@ -22,7 +22,6 @@ import java.util.concurrent.ThreadFactory; import com.xceptance.common.util.SynchronizingCounter; -import com.xceptance.common.util.concurrent.DaemonThreadFactory; import com.xceptance.xlt.api.engine.Session; import com.xceptance.xlt.api.util.XltLogger; @@ -81,7 +80,8 @@ public RequestQueue(final XltWebClient webClient, final int threadCount) this.threadCount = threadCount; parallelModeEnabled = true; - final ThreadFactory threadFactory = new DaemonThreadFactory(i -> Session.getCurrent().getUserID() + "-pool-" + i); + // final ThreadFactory threadFactory = new DaemonThreadFactory(i -> Session.getCurrent().getUserID() + "-pool-" + i); + final ThreadFactory threadFactory = new XltThreadFactory(true, Session.getCurrent().getUserID() + "-pool-"); executorService = Executors.newFixedThreadPool(threadCount, threadFactory); ongoingRequestsCount = new SynchronizingCounter(0); diff --git a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java index 6e1a9429a..eae1c9f93 100644 --- a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java +++ b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java @@ -83,14 +83,23 @@ public class SessionImpl extends Session private static final String UNKNOWN_USER_NAME = "UnknownUser"; /** - * The Session instances keyed by thread group. + * All Session instances keyed by thread. */ - private static final Map sessions = new ConcurrentHashMap(101); + private static final Map sessions = new ConcurrentHashMap<>(101); /** - * Name of the removeUserInfoFromURL property. + * The Session instance of the current thread. */ + private static final InheritableThreadLocal session = new InheritableThreadLocal<>() { + @Override + protected SessionImpl initialValue() + { + // Thread.dumpStack(); + return new SessionImpl(XltPropertiesImpl.getInstance()); + } + }; + /** * Returns the Session instance for the calling thread. If no such instance exists yet, it will be created. * @@ -98,7 +107,11 @@ public class SessionImpl extends Session */ public static SessionImpl getCurrent() { - return getSessionForThread(Thread.currentThread()); + final SessionImpl sessionImpl = session.get(); + + sessions.putIfAbsent(Thread.currentThread(), sessionImpl); + + return sessionImpl; } /** @@ -109,44 +122,22 @@ public static SessionImpl getCurrent() */ public static SessionImpl removeCurrent() { - return sessions.remove(Thread.currentThread().getThreadGroup()); + final SessionImpl sess = session.get(); + + session.set(null); + sessions.remove(Thread.currentThread()); + + return sess; } /** - * Returns the Session instance for the given thread. If no such instance exists yet, it will be created. + * Returns the Session instance for the given thread. * * @return the Session instance for the given thread */ public static SessionImpl getSessionForThread(final Thread thread) { - final ThreadGroup threadGroup = thread.getThreadGroup(); - - if (threadGroup == null) - { - // the thread died in between so there is no session - return null; - } - else - { - SessionImpl s = sessions.get(threadGroup); - - if (s == null) - { - synchronized (threadGroup) - { - // check again because two threads might have waited at the - // sync block and the first one created the session already - s = sessions.get(threadGroup); - if (s == null) - { - s = new SessionImpl(XltPropertiesImpl.getInstance()); - sessions.put(threadGroup, s); - } - } - } - - return s; - } + return sessions.get(thread); } /** diff --git a/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java new file mode 100644 index 000000000..14029aa67 --- /dev/null +++ b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.xlt.engine; + +import java.lang.reflect.InvocationTargetException; +import java.util.Objects; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang3.SystemUtils; + +import com.xceptance.common.lang.ReflectionUtils; +import com.xceptance.xlt.api.util.XltException; + +public class XltThreadFactory implements ThreadFactory +{ + private final boolean useVirtualThreads; + + private final boolean inheritInheritableThreadLocals; + + private final String threadNamePrefix; + + private final AtomicInteger threadCounter = new AtomicInteger(); + + public XltThreadFactory(final boolean inheritInheritableThreadLocals, final String threadNamePrefix) + { + useVirtualThreads = false; + this.inheritInheritableThreadLocals = inheritInheritableThreadLocals; + this.threadNamePrefix = Objects.toString(threadNamePrefix, "XltThread-"); + } + + @Override + public Thread newThread(final Runnable r) + { + final String threadName = threadNamePrefix + threadCounter.getAndIncrement(); + + final Thread thread = createThread(r); + thread.setName(threadName); + + return thread; + } + + private Thread createThread(final Runnable runnable) + { + final Thread thread; + + if (useVirtualThreads && SystemUtils.IS_JAVA_21) + { + System.out.println("Creating virtual thread"); + + // This is what actually needs to be done here: + // thread = Thread.ofVirtual().inheritInheritableThreadLocals(false).unstarted(this::run); + + // To make the above Java 21 code compile on Java 11, use reflection. + try + { + // 1. get the virtual thread builder + // final ThreadBuilder threadBuilder = Thread.ofVirtual(); + final Object threadBuilder = ReflectionUtils.callStaticMethod(Thread.class, "ofVirtual"); + + // 2. set whether to inherit inheritable thread locals + // threadBuilder.inheritInheritableThreadLocals(inheritInheritableThreadLocals); + // ReflectionUtils.callMethod(threadBuilder, "inheritInheritableThreadLocals", + // inheritInheritableThreadLocals); + ReflectionUtils.getMethod(threadBuilder.getClass(), "inheritInheritableThreadLocals", boolean.class) + .invoke(threadBuilder, inheritInheritableThreadLocals); + + // 3. create the thread + // threadBuilder.unstarted(this::run); + // thread = (Thread) ReflectionUtils.callMethod(threadBuilder, "unstarted", runnable); + thread = (Thread) ReflectionUtils.getMethod(threadBuilder.getClass(), "unstarted", Runnable.class).invoke(threadBuilder, + runnable); + } + catch (final SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) + { + throw new XltException("Failed to create virtual thread", e); + } + } + else + { + System.out.println("Creating platform thread"); + + thread = new Thread(null, runnable, "", 0, inheritInheritableThreadLocals); + thread.setDaemon(true); + } + + return thread; + } +} From 896ebbfe387649fb0e8a0d568dc53818edbef917 Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:19:32 +0200 Subject: [PATCH 02/10] WIP --- .../xceptance/xlt/agent/LoadTestRunner.java | 5 + .../com/xceptance/xlt/engine/SessionImpl.java | 62 ++++--- .../xlt/engine/XltThreadFactory.java | 6 +- .../xlt/agent/DataRecordLoggingTest.java | 162 +++++++----------- 4 files changed, 108 insertions(+), 127 deletions(-) diff --git a/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java b/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java index 36eb227a3..e047b0c29 100644 --- a/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java +++ b/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java @@ -107,6 +107,11 @@ public LoadTestRunner(final TestUserConfiguration config, final AgentInfo agentI thread.setName(config.getUserId()); } + Thread getThread() + { + return thread; + } + public void start() { thread.start(); diff --git a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java index eae1c9f93..82e3d09df 100644 --- a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java +++ b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java @@ -27,6 +27,7 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.runners.model.MultipleFailureException; import com.xceptance.common.io.FileUtils; @@ -86,11 +87,26 @@ public class SessionImpl extends Session * All Session instances keyed by thread. */ private static final Map sessions = new ConcurrentHashMap<>(101); + // private static final Map sessions = new ConcurrentHashMap<>(101); /** * The Session instance of the current thread. */ - private static final InheritableThreadLocal session = new InheritableThreadLocal<>() { + private static final InheritableThreadLocal magic = new InheritableThreadLocal<>() + { + + @Override + protected String initialValue() + { + return RandomStringUtils.random(32); + } + }; + + /** + * The Session instance of the current thread. + */ + private static final InheritableThreadLocal session = new InheritableThreadLocal<>() + { @Override protected SessionImpl initialValue() @@ -99,7 +115,7 @@ protected SessionImpl initialValue() return new SessionImpl(XltPropertiesImpl.getInstance()); } }; - + /** * Returns the Session instance for the calling thread. If no such instance exists yet, it will be created. * @@ -107,10 +123,14 @@ protected SessionImpl initialValue() */ public static SessionImpl getCurrent() { - final SessionImpl sessionImpl = session.get(); - - sessions.putIfAbsent(Thread.currentThread(), sessionImpl); - + SessionImpl sessionImpl = sessions.get(Thread.currentThread()); + + if (sessionImpl == null) + { + sessionImpl = session.get(); + sessions.put(Thread.currentThread(), sessionImpl); + } + return sessionImpl; } @@ -122,12 +142,8 @@ public static SessionImpl getCurrent() */ public static SessionImpl removeCurrent() { - final SessionImpl sess = session.get(); - - session.set(null); - sessions.remove(Thread.currentThread()); - - return sess; + session.remove(); + return sessions.remove(Thread.currentThread()); } /** @@ -291,6 +307,7 @@ protected SessionImpl() this.shutdownListeners = null; this.transactionTimeout = 0; } + /** * Creates a new Session object. */ @@ -321,12 +338,11 @@ public SessionImpl(final XltPropertiesImpl properties) // create more session-specific helper objects requestHistory = new RequestHistory(this, properties); - this.isTransactionExpirationTimerEnabled = properties.getProperty(this, XltConstants.XLT_PACKAGE_PATH + ".abortLongRunningTransactions") - .map(Boolean::valueOf) - .orElse(false); - this.transactionTimeout = properties.getProperty(this, PROP_MAX_TRANSACTION_TIMEOUT) - .flatMap(ParseNumbers::parseOptionalInt) - .orElse(DEFAULT_TRANSACTION_TIMEOUT); + this.isTransactionExpirationTimerEnabled = properties.getProperty(this, + XltConstants.XLT_PACKAGE_PATH + ".abortLongRunningTransactions") + .map(Boolean::valueOf).orElse(false); + this.transactionTimeout = properties.getProperty(this, PROP_MAX_TRANSACTION_TIMEOUT).flatMap(ParseNumbers::parseOptionalInt) + .orElse(DEFAULT_TRANSACTION_TIMEOUT); } /** @@ -387,8 +403,9 @@ public void clear() transactionTimer = null; // just for safety's sake valueLog.clear(); - // we cannot reset the name, because the session is recycled over and over again but never fully inited by the load test framework again - //userName = UNKNOWN_USER_NAME; + // we cannot reset the name, because the session is recycled over and over again but never fully inited by + // the load test framework again + // userName = UNKNOWN_USER_NAME; dataManagerImpl.close(); } @@ -483,7 +500,7 @@ public Path getResultsDirectory() // create new file handle for result directory rooted at the // user name directory which itself is rooted at the configured // result dir - // resultDir = new File(new File(resultDirName, cleanUserName), String.valueOf(userNumber)); + // resultDir = new File(new File(resultDirName, cleanUserName), String.valueOf(userNumber)); resultDir = Path.of(resultDirName, cleanUserName, String.valueOf(userNumber)); if (!Files.exists(resultDir)) @@ -498,8 +515,7 @@ public Path getResultsDirectory() } catch (IOException e) { - XltLogger.runTimeLogger.error("Cannot create file for output of timer: " - + resultDir.toString(), e); + XltLogger.runTimeLogger.error("Cannot create file for output of timer: " + resultDir.toString(), e); return null; } diff --git a/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java index 14029aa67..41a7ee661 100644 --- a/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java +++ b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java @@ -45,9 +45,9 @@ public XltThreadFactory(final boolean inheritInheritableThreadLocals, final Stri @Override public Thread newThread(final Runnable r) { - final String threadName = threadNamePrefix + threadCounter.getAndIncrement(); - final Thread thread = createThread(r); + + final String threadName = threadNamePrefix + threadCounter.getAndIncrement(); thread.setName(threadName); return thread; @@ -62,7 +62,7 @@ private Thread createThread(final Runnable runnable) System.out.println("Creating virtual thread"); // This is what actually needs to be done here: - // thread = Thread.ofVirtual().inheritInheritableThreadLocals(false).unstarted(this::run); + // thread = Thread.ofVirtual().inheritInheritableThreadLocals(false).unstarted(runnable); // To make the above Java 21 code compile on Java 11, use reflection. try diff --git a/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java b/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java index 6cedba485..db9c15de0 100644 --- a/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java +++ b/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java @@ -86,6 +86,7 @@ import com.xceptance.xlt.api.util.XltProperties; import com.xceptance.xlt.engine.DataManagerImpl; import com.xceptance.xlt.engine.SessionImpl; +import com.xceptance.xlt.engine.XltThreadFactory; import com.xceptance.xlt.engine.XltWebClient; import util.xlt.IntentionalError; @@ -107,7 +108,10 @@ { SessionImpl.class, DataManagerImpl.class, GlobalClock.class, AbstractExecutionTimer.class }) -@PowerMockIgnore({"javax.*", "org.xml.*", "org.w3c.dom.*"}) +@PowerMockIgnore( + { + "javax.*", "org.xml.*", "org.w3c.dom.*" +}) public class DataRecordLoggingTest { /** @@ -164,7 +168,7 @@ public void initMocks() throws Exception public void clear() { dataRecordCaptors.clear(); - mockDataManagers.clear(); + mockDataManager = null; } @Test @@ -200,12 +204,9 @@ protected void postValidate() throws Exception } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); - startAndWaitFor(testExecutionThread); - verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), - expect(RequestData.class, hasName("Action1.1"), hasFailed(false), hasUrl(url1)), + verifyDataRecordsLoggedWith(mockDataManager, expect(RequestData.class, hasName("Action1.1"), hasFailed(false), hasUrl(url1)), expect(ActionData.class, hasName("Action1"), hasFailed(false)), expect(EventData.class, hasName("Event 1"), hasMessage("Message 1"), hasTestCaseName(expectedUserName())), @@ -239,12 +240,9 @@ public void test() throws Throwable } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); - startAndWaitFor(testExecutionThread); - verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), - expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)), + verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)), expect(ActionData.class, hasName("FailedAction-Uncaught"), hasFailed(true)), // failedActionName is only set once expect(TransactionData.class, hasFailed(true), hasFailedActionName("FailedAction-Caught"))); @@ -267,11 +265,9 @@ public void test() throws Throwable } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); - startAndWaitFor(testExecutionThread); - verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), expect(1, ActionData.class, hasName("FirstAction"), hasFailed(false)), + verifyDataRecordsLoggedWith(mockDataManager, expect(1, ActionData.class, hasName("FirstAction"), hasFailed(false)), expect(0, ActionData.class, hasName("NotApplicableAction")), expect(1, ActionData.class, hasName("LastAction"), hasFailed(false)), expect(1, TransactionData.class, hasFailed(false), hasFailedActionName(null), @@ -297,12 +293,9 @@ public void test() throws Throwable } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); - startAndWaitFor(testExecutionThread); - verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), - expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)), + verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)), expect(ActionData.class, hasName("LastAction"), hasFailed(false)), expect(TransactionData.class, hasFailed(false), hasFailureStackTrace(null), hasFailedActionName("FailedAction-Caught"))); @@ -326,12 +319,9 @@ public void test() throws Throwable } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); - startAndWaitFor(testExecutionThread); - verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), - expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)), + verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)), expect(TransactionData.class, hasFailed(false), hasFailureStackTrace(null), hasFailedActionName("FailedAction-Caught"))); } @@ -349,21 +339,18 @@ public void test() throws Throwable } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); - startAndWaitFor(testExecutionThread); - final DataManagerImpl instance = mockDataManagerFor(threadGroup); if (kindOfLoadTestClass.isXltDerived() || testExecutionThreadStrategy.usesLoadTestRunner) { - verifyDataRecordsLoggedWith(instance, expect(0, ActionData.class), + verifyDataRecordsLoggedWith(mockDataManager, expect(0, ActionData.class), expect(TransactionData.class, hasName(expectedUserName()), hasFailed(true), hasFailedActionName(""), hasFailureStackTraceMatching(expectedFailureStacktraceRegex(STACKTRACE_REGEX_FOR_THROW_INTENTIONAL_ERROR, defaultUserId())))); } else { - Assert.assertNull("No action -> no session -> no data manager", instance); + Assert.assertNull("No action -> no session -> no data manager", mockDataManager); } } @@ -381,12 +368,9 @@ public void test() throws Throwable } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); - startAndWaitFor(testExecutionThread); - final DataManagerImpl instance = mockDataManagerFor(threadGroup); - verifyDataRecordsLoggedWith(instance, expect(1, ActionData.class, hasName("FirstAction"), hasFailed(false)), + verifyDataRecordsLoggedWith(mockDataManager, expect(1, ActionData.class, hasName("FirstAction"), hasFailed(false)), expect(TransactionData.class, hasName(expectedUserName()), hasFailed(true), hasFailedActionName(""), hasFailureStackTraceMatching(expectedFailureStacktraceRegex(STACKTRACE_REGEX_FOR_THROW_INTENTIONAL_ERROR, defaultUserId())))); @@ -413,11 +397,9 @@ public void test() throws Throwable } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); - startAndWaitFor(testExecutionThread); - verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), expect(ActionData.class, hasName("FirstAction"), hasFailed(false)), + verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FirstAction"), hasFailed(false)), expect(ActionData.class, hasName("FailedAction-preValidate"), hasFailed(true)), expect(TransactionData.class, hasName(expectedUserName()), hasFailed(true), hasFailedActionName("FailedAction-preValidate"), @@ -439,11 +421,9 @@ public void test() throws Throwable } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); - startAndWaitFor(testExecutionThread); - verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), expect(ActionData.class, hasName("FirstAction"), hasFailed(false)), + verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FirstAction"), hasFailed(false)), expect(ActionData.class, hasName("FailedAction-execute"), hasFailed(true)), expect(TransactionData.class, hasName(expectedUserName()), hasFailed(true), hasFailedActionName("FailedAction-execute"), @@ -465,11 +445,9 @@ public void test() throws Throwable } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); - startAndWaitFor(testExecutionThread); - verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), expect(ActionData.class, hasName("FirstAction"), hasFailed(false)), + verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FirstAction"), hasFailed(false)), expect(ActionData.class, hasName("FailedAction-postValidate"), hasFailed(true)), expect(TransactionData.class, hasName(expectedUserName()), hasFailed(true), hasFailedActionName("FailedAction-postValidate"), @@ -519,16 +497,14 @@ public void test() throws Throwable } }); - final ThreadGroup threadGroup = testExecutionThread.getThreadGroup(); GlobalClock.installFixed(startTime); startAndWaitFor(testExecutionThread); - final DataManagerImpl dataManager = mockDataManagerFor(threadGroup); - final InOrder inOrder = Mockito.inOrder(dataManager); - inOrder.verify(dataManager).setStartOfLoggingPeriod(startTime + initialDelay + warmUpPeriod); - inOrder.verify(dataManager).setEndOfLoggingPeriod(startTime + initialDelay + warmUpPeriod + measurementPeriod); - inOrder.verify(dataManager).logDataRecord(argThat(has(EventData.class, hasTime(eventTime), hasName(eventName)))); + final InOrder inOrder = Mockito.inOrder(mockDataManager); + inOrder.verify(mockDataManager).setStartOfLoggingPeriod(startTime + initialDelay + warmUpPeriod); + inOrder.verify(mockDataManager).setEndOfLoggingPeriod(startTime + initialDelay + warmUpPeriod + measurementPeriod); + inOrder.verify(mockDataManager).logDataRecord(argThat(has(EventData.class, hasTime(eventTime), hasName(eventName)))); } static final Pattern EOL_PLACEHOLDER_PATTERN = Pattern.compile(EOL_PLACEHOLDER_IN_STACKTRACE_REGEXES); @@ -644,7 +620,7 @@ public static void throwIntentionalError(Long timeOfFailure) throw new IntentionalError(); } - private Map mockDataManagers = createThreadSafeWeakHashMap(); + private DataManagerImpl mockDataManager; private Map> dataRecordCaptors = createThreadSafeWeakHashMap(); @@ -663,27 +639,13 @@ public DataManagerImpl answer(InvocationOnMock invocation) throws Throwable { // limit to constructor new DataManagerImpl(Session) and avoid using (Session, Metrics) final DataManagerImpl instance = Whitebox.invokeConstructor(DataManagerImpl.class, invocation.getArguments()[0]); - return mockDataManagers.computeIfAbsent(Thread.currentThread().getThreadGroup(), __ -> createMockDataManager(instance)); + mockDataManager = createMockDataManager(instance); + + return mockDataManager; } }); } - /** - * Returns the mock {@link DataManagerImpl} that will be used by the {@link SessionImpl} object for the specified - * thread group. - *

- * ATTENTION: If using this in a test, it needs to be called before the thread has finished - * - * @param thread - * @param session - * @return mock {@link DataManagerImpl} object used by {@link SessionImpl} for the specified thread - * @see #mockDataManagerCreation() - */ - private DataManagerImpl mockDataManagerFor(ThreadGroup threadGroup) - { - return mockDataManagers.get(threadGroup); - } - private DataManagerImpl createMockDataManager(final DataManagerImpl instance) { final ArgumentCaptor dataRecordCaptor = ArgumentCaptor.forClass(Data.class); @@ -727,7 +689,6 @@ private static void startAndWaitFor(Thread... threads) throws InterruptedExcepti } } - @SuppressWarnings("serial") static class CountingMap extends LinkedHashMap { public void increaseValueFor(final Key key, final int increaseBy) @@ -915,15 +876,15 @@ public static void setGenericLoadTestImplementationFor(final Thread thread, fina enum KindOfLoadTestClass { - /** - * Use a load test class that is derived from {@link AbstractTestCase} - */ - XltDerived(GenericLoadTestClasses.XltDerived.class, true), + /** + * Use a load test class that is derived from {@link AbstractTestCase} + */ + XltDerived(GenericLoadTestClasses.XltDerived.class, true), - /** - * Use a load test class that is not derived from anything except Object - */ - NotDerived(GenericLoadTestClasses.NotDerived.class, false); + /** + * Use a load test class that is not derived from anything except Object + */ + NotDerived(GenericLoadTestClasses.NotDerived.class, false); private KindOfLoadTestClass(Class genericTestClassObject, boolean isXltDerived) { @@ -955,35 +916,34 @@ public boolean isXltDerived() enum TestExecutionThreadStrategy { - /** - * Use a {@link LoadTestRunner} thread to execute the load test class - */ - LoadTestRunner(true) - { - @Override - public Thread createThreadFor(Class loadTestClassObject, TestUserConfiguration testUserConfiguration, AgentInfo agentInfo, - DataRecordLoggingTest thisTestInstance) - { - return new LoadTestRunner(testUserConfiguration, agentInfo, dummyExecutionTimer()); - } - - }, - - /** - * Use a simple thread that will just call JUnit's - * {@linkplain Request#aClass(Class) Request.aClass(Class)}.getRunner().run(RunNotifier) to execute - * the load test class - */ - JUnitClassRequestRunner(false) - { - @Override - public Thread createThreadFor(Class loadTestClassObject, TestUserConfiguration testUserConfiguration, AgentInfo agentInfo, - DataRecordLoggingTest thisTestInstance) - { - final Runnable r = () -> Request.aClass(loadTestClassObject).getRunner().run(new RunNotifier()); - return new Thread(new ThreadGroup("JUnitRequestRunner"), r); - } - }; + /** + * Use a {@link LoadTestRunner} thread to execute the load test class + */ + LoadTestRunner(true) + { + @Override + public Thread createThreadFor(Class loadTestClassObject, TestUserConfiguration testUserConfiguration, AgentInfo agentInfo, + DataRecordLoggingTest thisTestInstance) + { + return new LoadTestRunner(testUserConfiguration, agentInfo, dummyExecutionTimer()).getThread(); + } + }, + + /** + * Use a simple thread that will just call JUnit's + * {@linkplain Request#aClass(Class) Request.aClass(Class)}.getRunner().run(RunNotifier) to execute + * the load test class + */ + JUnitClassRequestRunner(false) + { + @Override + public Thread createThreadFor(Class loadTestClassObject, TestUserConfiguration testUserConfiguration, AgentInfo agentInfo, + DataRecordLoggingTest thisTestInstance) + { + final Runnable r = () -> Request.aClass(loadTestClassObject).getRunner().run(new RunNotifier()); + return new XltThreadFactory(false, null).newThread(r); + } + }; private TestExecutionThreadStrategy(final boolean usesLoadTestRunner) { From 4d2e6f64b23430393987b32b7355227efa5e1f32 Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:20:31 +0200 Subject: [PATCH 03/10] update to Java 21 --- .../xlt/engine/XltThreadFactory.java | 36 ++----------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java index 41a7ee661..e09ee73b0 100644 --- a/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java +++ b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java @@ -15,16 +15,10 @@ */ package com.xceptance.xlt.engine; -import java.lang.reflect.InvocationTargetException; import java.util.Objects; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; -import org.apache.commons.lang3.SystemUtils; - -import com.xceptance.common.lang.ReflectionUtils; -import com.xceptance.xlt.api.util.XltException; - public class XltThreadFactory implements ThreadFactory { private final boolean useVirtualThreads; @@ -57,37 +51,11 @@ private Thread createThread(final Runnable runnable) { final Thread thread; - if (useVirtualThreads && SystemUtils.IS_JAVA_21) + if (useVirtualThreads) { System.out.println("Creating virtual thread"); - // This is what actually needs to be done here: - // thread = Thread.ofVirtual().inheritInheritableThreadLocals(false).unstarted(runnable); - - // To make the above Java 21 code compile on Java 11, use reflection. - try - { - // 1. get the virtual thread builder - // final ThreadBuilder threadBuilder = Thread.ofVirtual(); - final Object threadBuilder = ReflectionUtils.callStaticMethod(Thread.class, "ofVirtual"); - - // 2. set whether to inherit inheritable thread locals - // threadBuilder.inheritInheritableThreadLocals(inheritInheritableThreadLocals); - // ReflectionUtils.callMethod(threadBuilder, "inheritInheritableThreadLocals", - // inheritInheritableThreadLocals); - ReflectionUtils.getMethod(threadBuilder.getClass(), "inheritInheritableThreadLocals", boolean.class) - .invoke(threadBuilder, inheritInheritableThreadLocals); - - // 3. create the thread - // threadBuilder.unstarted(this::run); - // thread = (Thread) ReflectionUtils.callMethod(threadBuilder, "unstarted", runnable); - thread = (Thread) ReflectionUtils.getMethod(threadBuilder.getClass(), "unstarted", Runnable.class).invoke(threadBuilder, - runnable); - } - catch (final SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) - { - throw new XltException("Failed to create virtual thread", e); - } + thread = Thread.ofVirtual().inheritInheritableThreadLocals(false).unstarted(runnable); } else { From 49a3027e825d54be1fd3ae076f56adab6620ed5b Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:16:59 +0100 Subject: [PATCH 04/10] WIP --- .../com/xceptance/common/util/Holder.java | 63 +++++++++++++++ .../xceptance/xlt/agent/LoadTestRunner.java | 4 +- .../xceptance/xlt/engine/RequestQueue.java | 1 - .../com/xceptance/xlt/engine/SessionImpl.java | 38 +++------ .../xlt/engine/XltThreadFactory.java | 81 +++++++++++-------- 5 files changed, 125 insertions(+), 62 deletions(-) create mode 100644 src/main/java/com/xceptance/common/util/Holder.java diff --git a/src/main/java/com/xceptance/common/util/Holder.java b/src/main/java/com/xceptance/common/util/Holder.java new file mode 100644 index 000000000..158bbffda --- /dev/null +++ b/src/main/java/com/xceptance/common/util/Holder.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2005-2025 Xceptance Software Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xceptance.common.util; + +/** + */ +public class Holder +{ + /** + * The target stream. + */ + private T object; + + /** + * Creates a new StreamPump object and initializes it with the given source and target streams. + */ + public Holder() + { + } + + /** + * Creates a new StreamPump object and initializes it with the given source stream. The target stream is created + * from the specified file. + * + * @param in + * the source stream + */ + public Holder(final T object) + { + this.object = object; + } + + public T get() + { + return object; + } + + public void set(T object) + { + this.object = object; + } + + public T remove() + { + final T oldObject = object; + object = null; + + return oldObject; + } +} diff --git a/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java b/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java index 2eb79cb9c..ca74c44bc 100644 --- a/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java +++ b/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java @@ -51,7 +51,7 @@ public class LoadTestRunner /** * Class logger instance. */ - private static final XltThreadFactory XLT_THREAD_FACTORY = new XltThreadFactory(false, null); + private static final XltThreadFactory XLT_THREAD_FACTORY = new XltThreadFactory(false); /** * Configuration. @@ -79,7 +79,7 @@ public class LoadTestRunner private volatile boolean aborted; /** - * The main thread of this user runner. + * The main thread of this runner. */ private final Thread thread; diff --git a/src/main/java/com/xceptance/xlt/engine/RequestQueue.java b/src/main/java/com/xceptance/xlt/engine/RequestQueue.java index e2e93df31..e6ef535ac 100644 --- a/src/main/java/com/xceptance/xlt/engine/RequestQueue.java +++ b/src/main/java/com/xceptance/xlt/engine/RequestQueue.java @@ -80,7 +80,6 @@ public RequestQueue(final XltWebClient webClient, final int threadCount) this.threadCount = threadCount; parallelModeEnabled = true; - // final ThreadFactory threadFactory = new DaemonThreadFactory(i -> Session.getCurrent().getUserID() + "-pool-" + i); final ThreadFactory threadFactory = new XltThreadFactory(true, Session.getCurrent().getUserID() + "-pool-"); executorService = Executors.newFixedThreadPool(threadCount, threadFactory); diff --git a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java index 299c2c2f4..f215b264a 100644 --- a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java +++ b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java @@ -27,11 +27,11 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.runners.model.MultipleFailureException; import com.xceptance.common.io.FileUtils; import com.xceptance.common.lang.ParseNumbers; +import com.xceptance.common.util.Holder; import com.xceptance.common.util.ParameterCheckUtils; import com.xceptance.xlt.api.actions.AbstractAction; import com.xceptance.xlt.api.engine.GlobalClock; @@ -87,32 +87,16 @@ public class SessionImpl extends Session * All Session instances keyed by thread. */ private static final Map sessions = new ConcurrentHashMap<>(101); - // private static final Map sessions = new ConcurrentHashMap<>(101); /** * The Session instance of the current thread. */ - private static final InheritableThreadLocal magic = new InheritableThreadLocal<>() + private static final InheritableThreadLocal> sessionHolder = new InheritableThreadLocal<>() { - - @Override - protected String initialValue() - { - return RandomStringUtils.random(32); - } - }; - - /** - * The Session instance of the current thread. - */ - private static final InheritableThreadLocal session = new InheritableThreadLocal<>() - { - @Override - protected SessionImpl initialValue() + protected Holder initialValue() { - // Thread.dumpStack(); - return new SessionImpl(XltPropertiesImpl.getInstance()); + return new Holder<>(); } }; @@ -123,13 +107,16 @@ protected SessionImpl initialValue() */ public static SessionImpl getCurrent() { - SessionImpl sessionImpl = sessions.get(Thread.currentThread()); + final Holder holder = sessionHolder.get(); + SessionImpl sessionImpl = holder.get(); if (sessionImpl == null) { - sessionImpl = session.get(); - sessions.put(Thread.currentThread(), sessionImpl); + sessionImpl = new SessionImpl(new XltPropertiesImpl()); + holder.set(sessionImpl); } + + sessions.put(Thread.currentThread(), sessionImpl); return sessionImpl; } @@ -142,8 +129,9 @@ public static SessionImpl getCurrent() */ public static SessionImpl removeCurrent() { - session.remove(); - return sessions.remove(Thread.currentThread()); + sessions.remove(Thread.currentThread()); + + return sessionHolder.get().remove(); } /** diff --git a/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java index e09ee73b0..4ee856c73 100644 --- a/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java +++ b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java @@ -17,54 +17,67 @@ import java.util.Objects; import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; + +/** + * A {@link ThreadFactory} that creates either virtual or platform daemon threads, depending on what is configured. + */ public class XltThreadFactory implements ThreadFactory { - private final boolean useVirtualThreads; - - private final boolean inheritInheritableThreadLocals; - - private final String threadNamePrefix; + private static final String DEFAULT_THREAD_NAME_PREFIX = "XltThread-"; - private final AtomicInteger threadCounter = new AtomicInteger(); + /** + * The underlying thread factory. + */ + private final ThreadFactory threadFactory; - public XltThreadFactory(final boolean inheritInheritableThreadLocals, final String threadNamePrefix) + /** + * Creates a thread factory creating daemon threads that have the default name prefix and don't inherit inheritable thread-locals. + */ + public XltThreadFactory() { - useVirtualThreads = false; - this.inheritInheritableThreadLocals = inheritInheritableThreadLocals; - this.threadNamePrefix = Objects.toString(threadNamePrefix, "XltThread-"); + this(false, DEFAULT_THREAD_NAME_PREFIX); } - @Override - public Thread newThread(final Runnable r) + /** + * Creates a thread factory creating daemon threads that have the default name prefix. + * + * @param inheritInheritableThreadLocals + * whether or not the threads will inherit inheritable thread-locals + */ + public XltThreadFactory(final boolean inheritInheritableThreadLocals) { - final Thread thread = createThread(r); - - final String threadName = threadNamePrefix + threadCounter.getAndIncrement(); - thread.setName(threadName); - - return thread; + this(inheritInheritableThreadLocals, DEFAULT_THREAD_NAME_PREFIX); } - private Thread createThread(final Runnable runnable) + /** + * Creates a thread factory creating daemon threads. + * + * @param inheritInheritableThreadLocals + * whether or not the threads will inherit inheritable thread-locals + * @param threadNamePrefix + * an optional thread name prefix (a counter will be appended to it) + */ + public XltThreadFactory(final boolean inheritInheritableThreadLocals, @Nullable final String threadNamePrefix) { - final Thread thread; - - if (useVirtualThreads) - { - System.out.println("Creating virtual thread"); + // TODO + final boolean useVirtualThreads = true; - thread = Thread.ofVirtual().inheritInheritableThreadLocals(false).unstarted(runnable); - } - else - { - System.out.println("Creating platform thread"); + // set up the thread builder and create a thread-safe thread factory from it + final Thread.Builder threadBuilder = useVirtualThreads ? Thread.ofVirtual() : Thread.ofPlatform().daemon(); + threadBuilder.inheritInheritableThreadLocals(inheritInheritableThreadLocals); + threadBuilder.name(Objects.toString(threadNamePrefix, DEFAULT_THREAD_NAME_PREFIX), 0); - thread = new Thread(null, runnable, "", 0, inheritInheritableThreadLocals); - thread.setDaemon(true); - } + threadFactory = threadBuilder.factory(); + } - return thread; + /** + * {@inheritDoc} + */ + @Override + public Thread newThread(final Runnable r) + { + return threadFactory.newThread(r); } } From 47a4f2bde90195ca10e4f2d190e463690f703a0b Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:16:40 +0100 Subject: [PATCH 05/10] reverted accidentally committed files --- bin/agent.cmd | 2 -- build.properties | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/agent.cmd b/bin/agent.cmd index 9b5b0ecb7..9c0cd14d3 100644 --- a/bin/agent.cmd +++ b/bin/agent.cmd @@ -40,8 +40,6 @@ if exist "%JVM_CFG_FILE%" ( for /f "eol=# delims=" %%o in ('type "%JVM_CFG_FILE%"') do set JAVA_OPTIONS=!JAVA_OPTIONS! %%o ) -set JAVA_OPTIONS=%JAVA_OPTIONS% -XX:InitialRAMPercentage=20 -XX:MaxRAMPercentage=20 - :: run Java echo java %JAVA_OPTIONS% com.xceptance.xlt.agent.Main %* > results\agentCmdLine java %JAVA_OPTIONS% com.xceptance.xlt.agent.AgentMain %* diff --git a/build.properties b/build.properties index 9db01a19e..63eb4f2f2 100644 --- a/build.properties +++ b/build.properties @@ -33,7 +33,7 @@ resultbrowser.dir.target = ${classes.dir}/com/xceptance/xlt/engine/resultbrowser # Linux timerrecorder.chrome.executable = chromium # Windows -timerrecorder.chrome.executable = C:/Program Files (x86)/Google/Chrome/Application/chrome.exe +#timerrecorder.chrome.executable = C:/Program Files (x86)/Google/Chrome/Application/chrome.exe # macOS #timerrecorder.chrome.executable = /Applications/Google Chrome.app/Contents/MacOS/Google Chrome From 28337417850c4ab85cf69cf262a0b086369c67d2 Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:13:06 +0100 Subject: [PATCH 06/10] try fixing unit tests --- ant-scripts/test.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/ant-scripts/test.xml b/ant-scripts/test.xml index ad7178e48..5501e04f6 100644 --- a/ant-scripts/test.xml +++ b/ant-scripts/test.xml @@ -315,6 +315,7 @@ + From c900fa34f5586a4bc7a7db0925019aed8ef61664 Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:43:16 +0100 Subject: [PATCH 07/10] try fixing unit tests --- .../xlt/agent/DataRecordLoggingTest.java | 97 ++++++++++--------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java b/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java index 6c7c04d38..0a25b3659 100644 --- a/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java +++ b/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java @@ -110,7 +110,7 @@ }) @PowerMockIgnore( { - "javax.*", "org.xml.*", "org.w3c.dom.*" + "javax.*", "org.xml.*", "org.w3c.dom.*", "org.apache.commons.vfs2.*" }) public class DataRecordLoggingTest { @@ -637,6 +637,13 @@ private void mockDataManagerCreation() throws Exception @Override public DataManagerImpl answer(InvocationOnMock invocation) throws Throwable { + // This method seems to be called immediately and with null parameters as part of setting up the answer + // (Powermock bug?) -> ignore. + if (invocation.getArgument(0, Session.class) == null) + { + return null; + } + // limit to constructor new DataManagerImpl(Session) and avoid using (Session, Metrics) final DataManagerImpl instance = Whitebox.invokeConstructor(DataManagerImpl.class, new Class[] { @@ -645,26 +652,28 @@ public DataManagerImpl answer(InvocationOnMock invocation) throws Throwable { invocation.getArgument(0, Session.class) }); - mockDataManager = createMockDataManager(instance); - return mockDataManager; + return createMockDataManager(instance); } }); } private DataManagerImpl createMockDataManager(final DataManagerImpl instance) { - final ArgumentCaptor dataRecordCaptor = ArgumentCaptor.forClass(Data.class); - final DataManagerImpl mock = Mockito.spy(instance); + // set up mock only if not done so before + if (mockDataManager == null) + { + final ArgumentCaptor dataRecordCaptor = ArgumentCaptor.forClass(Data.class); + final DataManagerImpl mock = Mockito.spy(instance); - Mockito.doNothing().when(mock).logDataRecord(dataRecordCaptor.capture()); + Mockito.doNothing().when(mock).logDataRecord(dataRecordCaptor.capture()); - // We want to see the logging of EventData records, so we'll have to let logEvent do its job - Mockito.doCallRealMethod().when(mock).logEvent(Mockito.any(), Mockito.any()); + dataRecordCaptors.put(mock, dataRecordCaptor); - dataRecordCaptors.put(mock, dataRecordCaptor); + mockDataManager = mock; + } - return mock; + return mockDataManager; } public List getDataRecordsCapturedFor(DataManager mockDataManager) @@ -882,15 +891,15 @@ public static void setGenericLoadTestImplementationFor(final Thread thread, fina enum KindOfLoadTestClass { - /** - * Use a load test class that is derived from {@link AbstractTestCase} - */ - XltDerived(GenericLoadTestClasses.XltDerived.class, true), + /** + * Use a load test class that is derived from {@link AbstractTestCase} + */ + XltDerived(GenericLoadTestClasses.XltDerived.class, true), - /** - * Use a load test class that is not derived from anything except Object - */ - NotDerived(GenericLoadTestClasses.NotDerived.class, false); + /** + * Use a load test class that is not derived from anything except Object + */ + NotDerived(GenericLoadTestClasses.NotDerived.class, false); private KindOfLoadTestClass(Class genericTestClassObject, boolean isXltDerived) { @@ -922,34 +931,34 @@ public boolean isXltDerived() enum TestExecutionThreadStrategy { - /** - * Use a {@link LoadTestRunner} thread to execute the load test class - */ - LoadTestRunner(true) - { - @Override - public Thread createThreadFor(Class loadTestClassObject, TestUserConfiguration testUserConfiguration, AgentInfo agentInfo, - DataRecordLoggingTest thisTestInstance) - { + /** + * Use a {@link LoadTestRunner} thread to execute the load test class + */ + LoadTestRunner(true) + { + @Override + public Thread createThreadFor(Class loadTestClassObject, TestUserConfiguration testUserConfiguration, AgentInfo agentInfo, + DataRecordLoggingTest thisTestInstance) + { return new LoadTestRunner(testUserConfiguration, agentInfo, dummyExecutionTimer()).getThread(); - } - }, - - /** - * Use a simple thread that will just call JUnit's - * {@linkplain Request#aClass(Class) Request.aClass(Class)}.getRunner().run(RunNotifier) to execute - * the load test class - */ - JUnitClassRequestRunner(false) - { - @Override - public Thread createThreadFor(Class loadTestClassObject, TestUserConfiguration testUserConfiguration, AgentInfo agentInfo, - DataRecordLoggingTest thisTestInstance) - { - final Runnable r = () -> Request.aClass(loadTestClassObject).getRunner().run(new RunNotifier()); + } + }, + + /** + * Use a simple thread that will just call JUnit's + * {@linkplain Request#aClass(Class) Request.aClass(Class)}.getRunner().run(RunNotifier) to execute + * the load test class + */ + JUnitClassRequestRunner(false) + { + @Override + public Thread createThreadFor(Class loadTestClassObject, TestUserConfiguration testUserConfiguration, AgentInfo agentInfo, + DataRecordLoggingTest thisTestInstance) + { + final Runnable r = () -> Request.aClass(loadTestClassObject).getRunner().run(new RunNotifier()); return new XltThreadFactory(false, null).newThread(r); - } - }; + } + }; private TestExecutionThreadStrategy(final boolean usesLoadTestRunner) { From ead2447c0686318c1c90f568ee37b2530a15addc Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:14:00 +0100 Subject: [PATCH 08/10] WIP --- .../com/xceptance/common/util/Holder.java | 31 ++++++++++--------- .../com/xceptance/xlt/engine/SessionImpl.java | 20 ++++++++---- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/xceptance/common/util/Holder.java b/src/main/java/com/xceptance/common/util/Holder.java index 158bbffda..3a2cd818c 100644 --- a/src/main/java/com/xceptance/common/util/Holder.java +++ b/src/main/java/com/xceptance/common/util/Holder.java @@ -16,48 +16,49 @@ package com.xceptance.common.util; /** + * Simple Holder class that holds a value. Use this class to imitate a mutable reference. */ public class Holder { /** - * The target stream. + * The value. */ - private T object; + private T value; /** - * Creates a new StreamPump object and initializes it with the given source and target streams. + * Creates a new Holder instance and initializes it with a null value. */ public Holder() { + this(null); } /** - * Creates a new StreamPump object and initializes it with the given source stream. The target stream is created - * from the specified file. + * Creates a new Holder instance and initializes it with the given value. * - * @param in - * the source stream + * @param value + * the initial value of the holder */ - public Holder(final T object) + public Holder(final T value) { - this.object = object; + this.value = value; } public T get() { - return object; + return value; } - public void set(T object) + public void set(T value) { - this.object = object; + this.value = value; } public T remove() { - final T oldObject = object; - object = null; + final T oldValue = value; + value = null; - return oldObject; + return oldValue; } } diff --git a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java index f215b264a..1853e2c5f 100644 --- a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java +++ b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java @@ -108,15 +108,22 @@ protected Holder initialValue() public static SessionImpl getCurrent() { final Holder holder = sessionHolder.get(); - SessionImpl sessionImpl = holder.get(); + SessionImpl sessionImpl = holder.get(); if (sessionImpl == null) { - sessionImpl = new SessionImpl(new XltPropertiesImpl()); - holder.set(sessionImpl); + synchronized (holder) + { + if (sessionImpl == null) + { + sessionImpl = new SessionImpl(XltPropertiesImpl.getInstance()); + holder.set(sessionImpl); + } + } } - - sessions.put(Thread.currentThread(), sessionImpl); + + // TODO + sessions.putIfAbsent(Thread.currentThread(), sessionImpl); return sessionImpl; } @@ -129,8 +136,9 @@ public static SessionImpl getCurrent() */ public static SessionImpl removeCurrent() { + // TODO: remove for all threads sessions.remove(Thread.currentThread()); - + return sessionHolder.get().remove(); } From 569e95a5ca5a67c0229e619b2e7899061574e509 Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:01:12 +0100 Subject: [PATCH 09/10] polishing --- .../config/default.properties | 3 ++ .../config/default.properties | 3 ++ .../config/default.properties | 3 ++ .../config/default.properties | 3 ++ .../testsuite-xlt/config/default.properties | 3 ++ .../com/xceptance/common/util/Holder.java | 18 +++++++++- .../com/xceptance/xlt/agent/LoadTest.java | 19 ++++++++--- .../xceptance/xlt/agent/LoadTestRunner.java | 25 ++++++++++---- .../xceptance/xlt/common/XltConstants.java | 6 ++++ .../xceptance/xlt/engine/RequestQueue.java | 6 ++-- .../com/xceptance/xlt/engine/SessionImpl.java | 10 +++--- .../xlt/engine/XltThreadFactory.java | 33 +++++++++++-------- .../xceptance/xlt/engine/XltWebClient.java | 6 +++- 13 files changed, 106 insertions(+), 32 deletions(-) diff --git a/samples/testsuite-performance/config/default.properties b/samples/testsuite-performance/config/default.properties index ac1a464bb..0348441ab 100644 --- a/samples/testsuite-performance/config/default.properties +++ b/samples/testsuite-performance/config/default.properties @@ -428,6 +428,9 @@ com.xceptance.xlt.maximumTransactionRunTime = 900000 ## Useful in case of severe server errors, etc. com.xceptance.xlt.maxErrors = 1000 +## Whether to use virtual threads instead of platform threads (default: false). +#com.xceptance.xlt.virtualThreads.enabled = false + ################################################################################ # diff --git a/samples/testsuite-posters/config/default.properties b/samples/testsuite-posters/config/default.properties index d356d64f3..a03d766fe 100644 --- a/samples/testsuite-posters/config/default.properties +++ b/samples/testsuite-posters/config/default.properties @@ -428,6 +428,9 @@ com.xceptance.xlt.maximumTransactionRunTime = 900000 ## Useful in case of severe server errors, etc. com.xceptance.xlt.maxErrors = 1000 +## Whether to use virtual threads instead of platform threads (default: false). +#com.xceptance.xlt.virtualThreads.enabled = false + ################################################################################ # diff --git a/samples/testsuite-showcases/config/default.properties b/samples/testsuite-showcases/config/default.properties index 1579ab7d4..40704973e 100644 --- a/samples/testsuite-showcases/config/default.properties +++ b/samples/testsuite-showcases/config/default.properties @@ -428,6 +428,9 @@ com.xceptance.xlt.maximumTransactionRunTime = 900000 ## Useful in case of severe server errors, etc. com.xceptance.xlt.maxErrors = 1000 +## Whether to use virtual threads instead of platform threads (default: false). +#com.xceptance.xlt.virtualThreads.enabled = false + ################################################################################ # diff --git a/samples/testsuite-template/config/default.properties b/samples/testsuite-template/config/default.properties index ac1a464bb..0348441ab 100644 --- a/samples/testsuite-template/config/default.properties +++ b/samples/testsuite-template/config/default.properties @@ -428,6 +428,9 @@ com.xceptance.xlt.maximumTransactionRunTime = 900000 ## Useful in case of severe server errors, etc. com.xceptance.xlt.maxErrors = 1000 +## Whether to use virtual threads instead of platform threads (default: false). +#com.xceptance.xlt.virtualThreads.enabled = false + ################################################################################ # diff --git a/samples/testsuite-xlt/config/default.properties b/samples/testsuite-xlt/config/default.properties index 1579ab7d4..40704973e 100644 --- a/samples/testsuite-xlt/config/default.properties +++ b/samples/testsuite-xlt/config/default.properties @@ -428,6 +428,9 @@ com.xceptance.xlt.maximumTransactionRunTime = 900000 ## Useful in case of severe server errors, etc. com.xceptance.xlt.maxErrors = 1000 +## Whether to use virtual threads instead of platform threads (default: false). +#com.xceptance.xlt.virtualThreads.enabled = false + ################################################################################ # diff --git a/src/main/java/com/xceptance/common/util/Holder.java b/src/main/java/com/xceptance/common/util/Holder.java index 3a2cd818c..1f324c5da 100644 --- a/src/main/java/com/xceptance/common/util/Holder.java +++ b/src/main/java/com/xceptance/common/util/Holder.java @@ -16,7 +16,7 @@ package com.xceptance.common.util; /** - * Simple Holder class that holds a value. Use this class to imitate a mutable reference. + * Simple Holder class that holds a value. Use this class to mimic a mutable reference. */ public class Holder { @@ -44,16 +44,32 @@ public Holder(final T value) this.value = value; } + /** + * Returns the current value. + * + * @return the value + */ public T get() { return value; } + /** + * Sets the new value. + * + * @param value + * the new value + */ public void set(T value) { this.value = value; } + /** + * Removes the current value. + * + * @return the value just removed + */ public T remove() { final T oldValue = value; diff --git a/src/main/java/com/xceptance/xlt/agent/LoadTest.java b/src/main/java/com/xceptance/xlt/agent/LoadTest.java index e4ec42c61..a1924ed96 100644 --- a/src/main/java/com/xceptance/xlt/agent/LoadTest.java +++ b/src/main/java/com/xceptance/xlt/agent/LoadTest.java @@ -24,6 +24,7 @@ import com.xceptance.xlt.agentcontroller.TestUserConfiguration; import com.xceptance.xlt.api.util.XltProperties; import com.xceptance.xlt.common.XltConstants; +import com.xceptance.xlt.engine.XltThreadFactory; /** * Class responsible for running a load test. @@ -37,6 +38,12 @@ public class LoadTest */ private static final long DEFAULT_GRACE_PERIOD = 30 * 1000; + /** + * The thread factory that creates either virtual threads or platform threads for the {@link LoadTestRunner} + * instances. + */ + private static final XltThreadFactory xltThreadFactory; + /** * The configured time period [ms] to wait for threads to finish voluntarily before quitting the JVM. */ @@ -44,10 +51,15 @@ public class LoadTest static { - final long v = XltProperties.getInstance().getProperty(XltConstants.XLT_PACKAGE_PATH + ".hangingUsersGracePeriod", - DEFAULT_GRACE_PERIOD); + final XltProperties props = XltProperties.getInstance(); + // grace period + final long v = props.getProperty(XltConstants.XLT_PACKAGE_PATH + ".hangingUsersGracePeriod", DEFAULT_GRACE_PERIOD); gracePeriod = (v < 0) ? DEFAULT_GRACE_PERIOD : v; + + // thread factory + final boolean useVirtualThreads = props.getProperty(XltConstants.PROP_VIRTUAL_THREADS_ENABLED, false); + xltThreadFactory = new XltThreadFactory(useVirtualThreads, false); } /** @@ -85,8 +97,7 @@ public void run() final AbstractExecutionTimer timer = ExecutionTimerFactory.createTimer(config); // create runner for configuration - final LoadTestRunner runner = new LoadTestRunner(config, agentInfo, timer); - //runner.setDaemon(true); + final LoadTestRunner runner = new LoadTestRunner(config, agentInfo, timer, xltThreadFactory); // add runner to list of known runners testRunners.add(runner); diff --git a/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java b/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java index ca74c44bc..07c95e609 100644 --- a/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java +++ b/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java @@ -48,11 +48,6 @@ public class LoadTestRunner */ private static final Logger log = LoggerFactory.getLogger(LoadTestRunner.class); - /** - * Class logger instance. - */ - private static final XltThreadFactory XLT_THREAD_FACTORY = new XltThreadFactory(false); - /** * Configuration. */ @@ -93,8 +88,11 @@ public class LoadTestRunner * load test agent information * @param timer * the execution timer that controls this load test runner + * @param xltThreadFactory + * the thread factory to create the worker thread */ - public LoadTestRunner(final TestUserConfiguration config, final AgentInfo agentInfo, final AbstractExecutionTimer timer) + public LoadTestRunner(final TestUserConfiguration config, final AgentInfo agentInfo, final AbstractExecutionTimer timer, + final XltThreadFactory xltThreadFactory) { this.config = config; this.agentInfo = agentInfo; @@ -103,20 +101,33 @@ public LoadTestRunner(final TestUserConfiguration config, final AgentInfo agentI status = new TestUserStatus(); status.setUserName(config.getUserId()); - thread = XLT_THREAD_FACTORY.newThread(this::run); + thread = xltThreadFactory.newThread(this::run); thread.setName(config.getUserId()); } + /** + * Returns the thread this runner is using under the hood. + *

+ * Note: For unit-testing only. + * + * @return the thread + */ Thread getThread() { return thread; } + /** + * Starts this runner. + */ public void start() { thread.start(); } + /** + * Waits until this runner is finished. + */ public void join() throws InterruptedException { thread.join(); diff --git a/src/main/java/com/xceptance/xlt/common/XltConstants.java b/src/main/java/com/xceptance/xlt/common/XltConstants.java index 81c237e07..4c31de3d3 100644 --- a/src/main/java/com/xceptance/xlt/common/XltConstants.java +++ b/src/main/java/com/xceptance/xlt/common/XltConstants.java @@ -509,4 +509,10 @@ private XltConstants() * The name of the HTML output file for rendering the scorecard report page. */ public static final String SCORECARD_REPORT_HTML_FILENAME = "scorecard.html"; + + /* + * Virtual threads + */ + + public static final String PROP_VIRTUAL_THREADS_ENABLED = XLT_PACKAGE_PATH + ".virtualThreads.enabled"; } diff --git a/src/main/java/com/xceptance/xlt/engine/RequestQueue.java b/src/main/java/com/xceptance/xlt/engine/RequestQueue.java index e6ef535ac..bc2b23a71 100644 --- a/src/main/java/com/xceptance/xlt/engine/RequestQueue.java +++ b/src/main/java/com/xceptance/xlt/engine/RequestQueue.java @@ -73,14 +73,16 @@ public class RequestQueue * the web client to use * @param threadCount * the number of threads + * @param useVirtualThreads + * whether to use virtual threads instead of platform threads */ - public RequestQueue(final XltWebClient webClient, final int threadCount) + public RequestQueue(final XltWebClient webClient, final int threadCount, final boolean useVirtualThreads) { this.webClient = webClient; this.threadCount = threadCount; parallelModeEnabled = true; - final ThreadFactory threadFactory = new XltThreadFactory(true, Session.getCurrent().getUserID() + "-pool-"); + final ThreadFactory threadFactory = new XltThreadFactory(useVirtualThreads, true, Session.getCurrent().getUserID() + "-pool-"); executorService = Executors.newFixedThreadPool(threadCount, threadFactory); ongoingRequestsCount = new SynchronizingCounter(0); diff --git a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java index 1853e2c5f..33708b55d 100644 --- a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java +++ b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java @@ -33,6 +33,7 @@ import com.xceptance.common.lang.ParseNumbers; import com.xceptance.common.util.Holder; import com.xceptance.common.util.ParameterCheckUtils; +import com.xceptance.xlt.agent.AbstractExecutionTimer; import com.xceptance.xlt.api.actions.AbstractAction; import com.xceptance.xlt.api.engine.GlobalClock; import com.xceptance.xlt.api.engine.NetworkDataManager; @@ -84,12 +85,13 @@ public class SessionImpl extends Session private static final String UNKNOWN_USER_NAME = "UnknownUser"; /** - * All Session instances keyed by thread. + * All Session instances keyed by thread. Needed by {@link AbstractExecutionTimer} only. */ private static final Map sessions = new ConcurrentHashMap<>(101); /** - * The Session instance of the current thread. + * The Session instance of the current thread. We use a Holder in-between so the main thread and sub-threads can + * share the same session and also remove the session if needed. */ private static final InheritableThreadLocal> sessionHolder = new InheritableThreadLocal<>() { @@ -122,7 +124,7 @@ public static SessionImpl getCurrent() } } - // TODO + // TODO: would be cool to get rid of that sessions.putIfAbsent(Thread.currentThread(), sessionImpl); return sessionImpl; @@ -136,7 +138,7 @@ public static SessionImpl getCurrent() */ public static SessionImpl removeCurrent() { - // TODO: remove for all threads + // TODO: remove for all threads sharing the same session sessions.remove(Thread.currentThread()); return sessionHolder.get().remove(); diff --git a/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java index 4ee856c73..97df1e6ed 100644 --- a/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java +++ b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java @@ -21,54 +21,61 @@ import javax.annotation.Nullable; /** - * A {@link ThreadFactory} that creates either virtual or platform daemon threads, depending on what is configured. + * A {@link ThreadFactory} that creates either virtual or platform daemon threads. */ public class XltThreadFactory implements ThreadFactory { private static final String DEFAULT_THREAD_NAME_PREFIX = "XltThread-"; /** - * The underlying thread factory. + * The underlying thread factory that does the hard work. */ private final ThreadFactory threadFactory; /** - * Creates a thread factory creating daemon threads that have the default name prefix and don't inherit inheritable thread-locals. + * Creates a thread factory creating daemon threads that have the default name prefix and don't inherit inheritable + * thread-locals. + * + * @param createVirtualThreads + * whether to create virtual threads instead of platform threads */ - public XltThreadFactory() + public XltThreadFactory(boolean createVirtualThreads) { - this(false, DEFAULT_THREAD_NAME_PREFIX); + this(createVirtualThreads, false); } /** * Creates a thread factory creating daemon threads that have the default name prefix. * + * @param createVirtualThreads + * whether to create virtual threads instead of platform threads * @param inheritInheritableThreadLocals * whether or not the threads will inherit inheritable thread-locals */ - public XltThreadFactory(final boolean inheritInheritableThreadLocals) + public XltThreadFactory(boolean createVirtualThreads, final boolean inheritInheritableThreadLocals) { - this(inheritInheritableThreadLocals, DEFAULT_THREAD_NAME_PREFIX); + this(createVirtualThreads, inheritInheritableThreadLocals, DEFAULT_THREAD_NAME_PREFIX); } /** * Creates a thread factory creating daemon threads. * + * @param createVirtualThreads + * whether to create virtual threads instead of platform threads * @param inheritInheritableThreadLocals * whether or not the threads will inherit inheritable thread-locals * @param threadNamePrefix * an optional thread name prefix (a counter will be appended to it) */ - public XltThreadFactory(final boolean inheritInheritableThreadLocals, @Nullable final String threadNamePrefix) + public XltThreadFactory(boolean createVirtualThreads, final boolean inheritInheritableThreadLocals, + @Nullable final String threadNamePrefix) { - // TODO - final boolean useVirtualThreads = true; - - // set up the thread builder and create a thread-safe thread factory from it - final Thread.Builder threadBuilder = useVirtualThreads ? Thread.ofVirtual() : Thread.ofPlatform().daemon(); + // set up the thread builder + final Thread.Builder threadBuilder = createVirtualThreads ? Thread.ofVirtual() : Thread.ofPlatform().daemon(); threadBuilder.inheritInheritableThreadLocals(inheritInheritableThreadLocals); threadBuilder.name(Objects.toString(threadNamePrefix, DEFAULT_THREAD_NAME_PREFIX), 0); + // create a thread-safe thread factory from the thread builder threadFactory = threadBuilder.factory(); } diff --git a/src/main/java/com/xceptance/xlt/engine/XltWebClient.java b/src/main/java/com/xceptance/xlt/engine/XltWebClient.java index 6fd2d8131..15c6bf75d 100644 --- a/src/main/java/com/xceptance/xlt/engine/XltWebClient.java +++ b/src/main/java/com/xceptance/xlt/engine/XltWebClient.java @@ -87,6 +87,7 @@ import com.xceptance.xlt.api.util.XltException; import com.xceptance.xlt.api.util.XltLogger; import com.xceptance.xlt.api.util.XltProperties; +import com.xceptance.xlt.common.XltConstants; import com.xceptance.xlt.engine.htmlunit.apache.XltApacheHttpWebConnection; import com.xceptance.xlt.engine.htmlunit.okhttp3.OkHttp3WebConnection; import com.xceptance.xlt.engine.socket.XltSockets; @@ -288,7 +289,10 @@ public XltWebClient(final BrowserVersion browserVersion, final boolean javaScrip XltLogger.runTimeLogger.warn("Property 'com.xceptance.xlt.staticContent.downloadThreads' is set to an invalid value. Will use 1 instead."); threadCount = 1; } - requestQueue = new RequestQueue(this, threadCount); + + final boolean useVirtualThreads = props.getProperty(XltConstants.PROP_VIRTUAL_THREADS_ENABLED, false); + + requestQueue = new RequestQueue(this, threadCount, useVirtualThreads); /* * Configure the super class. From 8cc6d5ee6bbdd223da1428f6bbc0b62b71b9baf3 Mon Sep 17 00:00:00 2001 From: Joerg Werner <4639399+jowerner@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:46:13 +0100 Subject: [PATCH 10/10] * Mark all chart image tabs on the Custom Data and Agents page with the "img-tab" class as well. * In print mode, don't lazy load chart images. We need them right away. --- .../xsl/loadreport/sections/custom-values.xsl | 12 +++++------ config/xsl/loadreport/util/agent-chart.xsl | 18 ++++++++--------- config/xsl/loadreport/util/timer-chart.xsl | 20 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/config/xsl/loadreport/sections/custom-values.xsl b/config/xsl/loadreport/sections/custom-values.xsl index d2edb3624..625a7b9ce 100644 --- a/config/xsl/loadreport/sections/custom-values.xsl +++ b/config/xsl/loadreport/sections/custom-values.xsl @@ -151,17 +151,17 @@ chart-

Back to Table -
+
charts/customvalues/
-
+
charts/placeholder.webp @@ -190,11 +190,11 @@
Overview
- charts/customvalues/{$encodedChartFilename}.webp +
Averages
- charts/customvalues/{$encodedChartFilename}_Average.webp +
diff --git a/config/xsl/loadreport/util/agent-chart.xsl b/config/xsl/loadreport/util/agent-chart.xsl index 1a07e8c37..6c720bdf4 100644 --- a/config/xsl/loadreport/util/agent-chart.xsl +++ b/config/xsl/loadreport/util/agent-chart.xsl @@ -18,13 +18,13 @@
chart- @@ -35,7 +35,7 @@ -
+
charts/agents//CpuUsage.webp @@ -45,7 +45,7 @@
-
+
charts/placeholder.webp @@ -54,7 +54,7 @@
-
+
charts/placeholder.webp @@ -68,19 +68,19 @@
Memory
- charts/agents//MemoryUsage.webp + charts/agents//MemoryUsage.webp
CPU
- charts/agents//CpuUsage.webp + charts/agents//CpuUsage.webp
Threads
- charts/agents//Threads.webp + charts/agents//Threads.webp
diff --git a/config/xsl/loadreport/util/timer-chart.xsl b/config/xsl/loadreport/util/timer-chart.xsl index e76d6e482..7d9bca85e 100644 --- a/config/xsl/loadreport/util/timer-chart.xsl +++ b/config/xsl/loadreport/util/timer-chart.xsl @@ -158,44 +158,44 @@
Overview
- charts/{$directory}/{$encodedName}.webp +
Averages
- charts/{$directory}/{$encodedName}_Average.webp +
Count/s
- charts/{$directory}/{$encodedName}_CountPerSecond.webp +
Arrival Rate
- charts/{$directory}/{$encodedName}_ArrivalRate.webp +
Concurrent Users
- charts/{$directory}/{$encodedName}_ConcurrentUsers.webp +
Overview
- charts/{$directory}/{$encodedName}.webp +
Count/s
- charts/{$directory}/{$encodedName}_CountPerSecond.webp +
Averages
- charts/{$directory}/{$encodedName}_Average.webp +
Response Size
- charts/{$directory}/{$encodedName}_ResponseSize.webp +
Distribution
- charts/{$directory}/{$encodedName}_Histogram.webp +