Julio J. Gomez Diaz created QPIDJMS-635:
-------------------------------------------
Summary: Allow a pooled/reusable codec instead of the per-thread
AmqpCodec ThreadLocal
Key: QPIDJMS-635
URL: https://issues.apache.org/jira/browse/QPIDJMS-635
Project: Qpid JMS
Issue Type: Improvement
Components: qpid-jms-client
Affects Versions: 2.11.0
Reporter: Julio J. Gomez Diaz
h2. Description
{{org.apache.qpid.jms.provider.amqp.message.AmqpCodec}} keeps its
{{{}EncoderImpl{}}}/{{{}DecoderImpl{}}} in a {{{}private static final
ThreadLocal<EncoderDecoderContext> TLS_CODEC{}}}, created via
{{{}ThreadLocal.withInitial(...){}}}. {{AmqpCodec.encodeMessage(...)}} /
{{decodeMessage(...)}} and {{{}getEncoder(){}}}/{{{}getDecoder(){}}} all go
through {{{}TLS_CODEC.get(){}}}, so calling them *creates* the codec as a side
effect of the first use on a thread.
On the synchronous producer path ({{{}JmsSession.send{}}} →
{{AmqpJmsMessageFacade.encodeMessage}} → {{{}AmqpCodec.encodeMessage{}}}) the
encode runs on the {*}calling thread{*}. There is no offload, and there is no
{{remove()}} anywhere, so a thread that sends holds the codec until it dies.
With platform thread pools this is amortised. With virtual threads, each new VT
that sends pays the full creation cost.
h2. Measured cost
On a fresh virtual thread a single producer encode allocates ~16.3 KB
({{{}gc.alloc.rate.norm{}}}, JMH), vs ~0 B on a warm platform thread:
||Scenario||bytes/op||ns/op||
|Empty fresh VT baseline|~502|~41 000|
|qpid-jms encode on fresh VT ({{{}AmqpCodec.TLS_CODEC{}}})|~16 320|~83 800|
|qpid-jms encode on warm platform thread|~0|~9|
Measured against {{qpid-jms-client}} 2.11.0 + {{proton-j}} 0.35.0, real
producer {{send}} to an embedded Artemis AMQP acceptor, JDK 25.
h2. Precedent
Apache Fory solves exactly this by preferring a shared, bounded pool over a
ThreadLocal for virtual threads ({{{}buildThreadSafeFory(){}}} →
{{{}ThreadPoolFory{}}}, fixed size, slot preference with fallback to any free
slot, released on completion; {{buildThreadSafeForyPool(int)}} for an explicit
size). Kryo has the equivalent
{{{}com.esotericsoftware.kryo.kryo5.util.Pool<T>{}}}. gemmellr's own remark
above ("could actually do the same without that thread local") points the same
way.
h2. Proposal (opt-in, default-preserving)
Keep {{TLS_CODEC}} as the default. Add an opt-in hook so the codec can come
from a bounded pool instead of the ThreadLocal:
* A static provider/pool hook on {{{}AmqpCodec{}}}, e.g.
{{AmqpCodec.setCodecProvider(...)}} (or a connection-level pool), used by
{{{}encodeMessage{}}}/{{{}decodeMessage{}}}/{{{}getEncoder{}}}/{{{}getDecoder{}}}.
Unset → unchanged behaviour.
* The provider borrows an {{EncoderDecoderContext}} for the duration of the
call and returns it, capping memory at the pool size instead of at the number
of threads.
Additive; existing callers see no change.
Related: PROTON-2994 raises the same per-thread-codec concern for
MessageImpl.tlsCodec (proton-j), which serves the embedded/standalone and
Azure Event Hubs paths. The two are independent changes: qpid-jms does not
use proton's ThreadLocal on its send path, it has its own (AmqpCodec.
TLS_CODEC), so each fix stands on its own. Linked for shared context on
the pool-based alternative.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]