Yavdhesh opened a new pull request, #6855:
URL: https://github.com/apache/jmeter/pull/6855

   ## Problem
   
   JMeter creates one platform (OS) thread per concurrent virtual user. 
Platform threads are
   expensive: each carries a native stack and scheduler cost, so a load 
generator's user count is
   bounded by thread memory and context switching long before the target 
system's limits are
   reached — especially for the common case where user threads spend most of 
their time *blocked*
   (waiting for HTTP responses, timers, JDBC).
   
   ## Change
   
   Opt-in support for running JMeter user threads on **virtual threads** (Java 
21+), behind a new
   property, **off by default**:
   
       jmeterthread.use_virtual_threads=false   # default; set to true to enable
   
   * New `org.apache.jmeter.threads.JMeterThreadFactory` — the single place 
that creates the
     `Thread` for a `JMeterThread`. Returns an unstarted virtual thread when 
the property is on
     and the JVM supports it, otherwise exactly today's `new Thread(runnable, 
name)`.
   * `ThreadGroup` — the only two user-thread creation sites 
(`startNewThread(...)` and the
     delayed-start `ThreadStarter`) now go through the factory. The 
delayed-start path skips
     `setDaemon(false)` for virtual threads (they are always daemon; see 
caveats).
   * `bin/jmeter.properties` — documents the property.
   
   Nothing else changes: no executor, no thread pooling, no changes to 
`JMeterThread`, the engine,
   samplers, or the GUI.
   
   ## Compatibility (Java 17)
   
   JMeter is compiled with `--release 17`, so the Java 21 API 
(`Thread.ofVirtual()`,
   `Thread.isVirtual()`) is accessed **reflectively**, resolved once. Behaviour 
matrix:
   
   | Runtime | Property off (default) | Property on |
   |---|---|---|
   | Java 17 | platform threads (unchanged) | platform threads + one-time 
warning |
   | Java 21+ | platform threads (unchanged) | **virtual threads** |
   
   If reflective creation fails (e.g. a preview runtime without 
`--enable-preview`), the factory
   falls back to a platform thread with a warning. Default behaviour is 
byte-for-byte the current
   behaviour.
   
   ## Design notes / caveats
   
   * **ThreadLocal semantics preserved.** One dedicated thread per 
`JMeterThread`, exactly as
     today — threads are never pooled or shared between users, so 
`JMeterContextService`'s
     `ThreadLocal<JMeterContext>` behaves identically. (A shared-executor 
design was considered
     and rejected precisely because it would break this.)
   * **Completion/stop semantics preserved.** Threads are tracked in
     `ThreadGroup.allThreads` (`ConcurrentHashMap<JMeterThread, Thread>`); 
`threadFinished()`,
     `waitThreadsStopped()` (`join`/`isAlive`) and `stopThread()` 
(`interrupt()`) all work
     unchanged on virtual threads — covered by a dedicated unit test.
   * **Daemon threads.** Virtual threads are always daemon, and 
`setDaemon(false)` throws for
     them, so the delayed-start path only applies it to platform threads. The 
engine explicitly
     waits for user threads, and GUI mode has AWT threads, so nothing relied on 
non-daemon user
     threads to keep the JVM alive.
   * **Pinning (Java 21).** A virtual thread that holds a monitor 
(`synchronized`) while doing
     blocking I/O pins its carrier thread (addressed for `synchronized` in Java 
24, JEP 491).
     Samplers with such code may see reduced benefit; watch JFR 
`jdk.VirtualThreadPinned`.
     This is a reason the feature is opt-in.
   * **CPU-bound samplers gain nothing** — virtual threads help blocking 
workloads.
   
   ## Tests
   
   New `TestJMeterThreadFactory` (core test module):
   
   * property off (default) → platform thread, name preserved, start/join works;
   * property explicitly `false` → platform thread;
   * property `true` on Java 21 → the factory returns a virtual thread, the 
runnable is observed
     executing on a virtual thread, and the thread reports `isDaemon() == true` 
(the engine
     contract above); on Java < 21 the case is skipped via JUnit `Assumptions`;
   * a virtual thread blocked in `Thread.sleep` responds to `interrupt()` and 
can be `join()`ed
     (the `stopThread` contract).
   
   Targeted results: `TestJMeterThreadFactory` 4/4, existing `TestJMeterThread` 
3/3, plus
   neighbouring `TestJMeterContext`, `TestTestCompiler`, 
`TestUnmodifiableJMeterVariables`,
   `TestTreeCloner` 20/20 — **27/27 pass**, no regressions in the touched 
packages.
   Maintainers: `./gradlew :src:core:test --tests 
"org.apache.jmeter.threads.*"` and
   `./gradlew --quiet classes style` are the canonical checks (the 
contributor's sandbox could
   not run the Gradle daemon — its loopback-TCP channel is blocked by the 
sandbox network
   policy — so the modules were compiled and the tests executed directly with
   javac/kotlinc + the JUnit console at the BOM-pinned dependency versions; see 
the linked
   benchmark notes).
   
   ## Benchmark (measured, same build/plan/machine — only the property differs)
   
   Workload: each sample blocks 300 ms (the blocking-wait profile of an HTTP 
request against a
   300 ms-latency server); 10,000 samples per run; ramp-up 10 s; `-Xms256m 
-Xmx1g`;
   Temurin **21.0.13+9**; small shared VM (4 vCPUs visible, ~1.5 CPU shared 
quota, ~7.7 GiB RAM).
   Peak RSS / OS threads sampled from `/proc/<pid>/status` during the run.
   In the contributor's sandbox, Java TCP clients are policy-blocked (even on 
loopback), so the
   executed plan uses a Java Request sampler that sleeps 300 ms; an equivalent 
HTTP-sampler
   plan + delay server is included in the benchmark harness for unrestricted 
machines.
   
   | Users | Mode | Samples | Errors | Wall elapsed | Wall throughput | 
Summariser throughput | Peak RSS | Peak OS threads |
   |---:|---|---:|---:|---:|---:|---:|---:|---:|
   | 500 | platform (baseline) | 10,000 | 0 (0.00%) | 38 s | 263.2/s | 621.2/s 
| 723 MB | 525 |
   | 500 | virtual | 10,000 | 0 (0.00%) | 36 s | 277.8/s | 623.6/s | 685 MB | 
30 |
   | 2000 | platform (baseline) | 10,000 | 0 (0.00%) | 33 s | 303.0/s | 865.6/s 
| 784 MB | 2025 |
   | 2000 | virtual | 10,000 | 0 (0.00%) | 32 s | 312.5/s | 865.1/s | 694 MB | 
30 |
   
   Deltas at 2000 users: OS threads **2025 → 30 (−98.5%)**, peak RSS **784 → 
694 MB (−11.5%)**,
   throughput essentially unchanged (865.6 vs 865.1 samples/s in the summariser 
window — this
   workload is latency-bound, as expected). At 500 users: threads 525 → 30 
(−94.3%), RSS
   723 → 685 MB (−5.3%). The win is resource scaling: OS-thread count stays 
flat instead of
   growing 1:1 with users. Numbers are indicative from a small shared VM; on 
dedicated
   load-generator hardware the headroom gap would be larger, since 
platform-thread cost grows
   linearly with user count. Full methodology, raw outputs and honesty notes: 
BENCHMARK.md in
   the contributor's workspace (harness: DelayServer/GenPlan + 
run-benchmark.sh).
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to