Repository navigation
Use virtual threads by default on Java 21+ #56
Description
Activity
@edburns let me know what you think before I assign it to @copilot
@brunoborges this seems a lot more involved than what we already did for Virtual Threads here: #40 . I am worried about architectural integrity with this different approach.
As the DRI, I'm not comfortable proceeding with this approach just yet. Sure you can assign it to copilot, but I can't consent to merging the work it eventually does until I think about this some more.
The goal here is to allow Copilot SDK to default to VTs when running on JDK 21+ without requiring manual user configuration. They can still revert to non-VT in JDK 21+ if they want.
I thought you did want to have VT by default, and why you wanted to push the SDK to a 21+ baseline.
The proposal here is the perfect balance between JDK 17 baseline support, while defaulting to VT when the SDK runs on JDK 21+.
- linked a pull request that will close this issueUse virtual threads by default on Java 21+ via Multi-Release JAR #60
on Apr 7, 2026 Hi @brunoborges , I have performed my analysis on the necessity of fixing #56 with #60 . Below you can find the receipts. My conclusion is that accepting these changes is not worth the added complexity.
I am open to further debate on this matter, but if you want to re-open the debate, I request we have a human discussion about it before spending any tokens.
Receipts
13:26 Prompt
In this session, I aim to review PR #60 .
My first question: how PR 60, which ostensibly aims to "Use virtual threads by default on Java 21+ via Multi-Release JAR" relates to
- Accept optional Executor on CopilotClientOptions to mitigate pool starvation #40 Which added the capability to pass an
ExecutortoCopilotClientOptions. Especially important is the change toREADME.mdin that PR, which shows how to use this capability.
During the dev work for PR 40, Copilot told me that these changes were sufficient to provide Virtual Threads support. If that is true, what is the point of PR 60?
13:35 Response
Fetched 2 resources
Read , lines 1 to 50
Now let me look at the actual code in the workspace to understand the current state and the PR 60 branch changes.
Explore virtual thread code
Read
Searched for files matching
**/ThreadFactoryProvider.java, 2 matchesSearched for files matching
**/JsonRpcClient.java, 1 matchSearched for files matching
**/CliServerManager.java, 1 matchSearched for regex
virtual thread|virtual threads(**/README.md), 5 resultsSearched for files matching
**/CopilotClientOptions.java, 1 matchRead , lines 1 to 50
Read , lines 1 to 50
Read , lines 1 to 150
Read , lines 1 to 150
Read , lines 1 to 100
Read , lines 140 to 160
Read
Read , lines 1 to 60
Searched for text
startReader(**/src/main/java/com/github/copilot/sdk/JsonRpcClient.java), 2 resultsSearched for text
startStderrReader(**/src/main/java/com/github/copilot/sdk/CliServerManager.java), 2 resultsSearched for text
setExecutor(**/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java), 1 resultRead , lines 179 to 220
Read , lines 174 to 200
Read , lines 260 to 300
Here's my analysis of how the two PRs relate and whether PR 60 is justified:
What each PR covers
PR 40 wired a user-provided
Executorthrough allCompletableFuture.*Asynccall sites — tool handlers, permission handlers, session operations, client start/stop. Users opt in with one line:var options = new CopilotClientOptions() .setExecutor(Executors.newVirtualThreadPerTaskExecutor());
PR 60 adds a Multi-Release JAR (
ThreadFactoryProvider) that automatically switches two internal SDK daemon threads to virtual threads on Java 21+:jsonrpc-readerthread (inJsonRpcClient)cli-stderr-readerthread (inCliServerManager)
The contradiction
During PR 40, the Copilot analysis explicitly concluded these internal threads would not benefit from being virtual:
"The reader thread does blocking I/O on
ProcessInputStream— making it virtual wouldn't help (it's pinned on a native read syscall anyway)."
"The timeout scheduler must be aScheduledExecutorService; virtual threads don't improve timer-wheel behaviour."
"No API change needed here."PR 60 does exactly what PR 40's analysis said was unnecessary — making the reader and stderr threads virtual.
Is PR 60 justified?
Marginal benefit at best. The
jsonrpc-readerandcli-stderr-readerthreads block on native process I/O (InputStream.read()on a pipe). On JDK 21–23, virtual threads are pinned during native syscalls, so they behave identically to platform threads for this workload. On JDK 24+ (JEP 491),synchronizedpinning is fixed, but these threads aren't contending on monitors — they're blocked on I/O.The only real argument for PR 60 is resource overhead: virtual threads are cheaper than platform threads (no 1MB stack reservation). But the SDK only creates 2 daemon threads per client instance, so the savings are negligible.
Meanwhile, the meaningful virtual thread benefit — running many concurrent
CompletableFuturetasks without pool starvation — is already fully covered by PR 40'ssetExecutor().Summary
Concern PR 40 PR 60 CompletableFuture.*Asyncwork (tool/permission/session)Covered via setExecutor()Not addressed Internal I/O daemon threads (2 threads) Not addressed (analysis said unnecessary) Covered via MR-JAR User action required Yes — one line of code None — automatic Real-world performance impact High (fixes pool starvation) Negligible (2 threads doing pinned native I/O) Your Copilot was essentially right during PR 40: the
setExecutor()API is the meaningful virtual thread enabler. PR 60 adds complexity (MR-JAR build, dual source trees,ThreadFactoryProviderabstraction) for a marginal benefit on exactly the threads that the PR 40 analysis said wouldn't benefit. You should weigh whether the added build/maintenance complexity is worth the "zero-config" story for 2 internal daemon threads.- Accept optional Executor on CopilotClientOptions to mitigate pool starvation #40 Which added the capability to pass an
Summary
When running on Java 21+, the SDK should default to using virtual threads instead of platform threads for its internal thread pools and thread creation.
Motivation
Virtual threads (JEP 444, finalized in Java 21) are lightweight threads that can significantly reduce resource usage for I/O-bound workloads like the JSON-RPC communication this SDK performs. Since the SDK targets Java 17+, we can use Multi-Release JARs (JEP 238) to provide a Java 21+ implementation that uses virtual threads while keeping the Java 17 baseline implementation with platform threads.
Current thread usage
The SDK currently creates platform threads in several places:
JsonRpcClient—Executors.newSingleThreadExecutor()for the JSON-RPC reader loop, plus aThread(r, "jsonrpc-reader")factoryCopilotSession—ScheduledExecutorServiceforsendAndWaittimeouts, plusThread(r, "sendAndWait-timeout")factoryCliServerManager—Threadfor stderr forwardingProposed approach
ThreadFactoryProvider(or similar) abstraction insrc/main/java/that returns standard platform-thread factories (the Java 17 baseline)src/main/java21/that returns virtual-thread factories (Thread.ofVirtual().factory())META-INF/versions/21/new Thread()andExecutors.newSingleThreadExecutor()calls with the factory abstractionBuild conditionality
The second
maven-compiler-pluginexecution (compilingsrc/main/java21/with--release 21andmultiReleaseOutput=true) should be gated by a<jdk>[21,)</jdk>Maven profile so the project still builds cleanly on JDK 17 — the JAR just won't include the MR overlay. CI and release builds should use JDK 21+ to produce the full multi-release JAR.ScheduledThreadPoolExecutorstays with platform threadsThe JDK has no virtual-thread-based
ScheduledExecutorService. TheScheduledThreadPoolExecutorinCopilotSession(used forsendAndWaittimeouts) should remain with platform threads and is out of scope for this issue.Spotless coverage for
src/main/java21/The Spotless plugin config needs an explicit include for
src/main/java21/**/*.javaso the Java 21 override class gets formatted consistently.Documentation
Ensure that the following documentation is updated to clearly describe virtual threads support when running on Java 21+:
src/site/markdown/) — Mention virtual threads as a benefit of running on Java 21+ in the getting started flowjbang-example.java) — Ensure examples work seamlessly on both Java 17 (platform threads) and Java 21+ (virtual threads), and add comments or notes highlighting the behavior differencesrc/site/markdown/) — Add a dedicated section or page covering virtual threads support: what it means for users, performance implications, and any caveats (e.g.,ScheduledExecutorServiceremaining on platform threads)CopilotClient,CopilotSession) so users understand which threads are used at runtimeNotes
ScheduledExecutorServiceinCopilotSessiondoes not have a virtual-thread equivalent in the JDK — it may need to stay as-is or use an alternative scheduling approachForkJoinPoolcarrier; no custom carrier pool is neededThread.ofVirtual().name("jsonrpc-reader"))