Guillaume Nodet created CAMEL-23686:
---------------------------------------

             Summary: Reduce Exchange memory pressure and fix pooled exchange 
issues
                 Key: CAMEL-23686
                 URL: https://issues.apache.org/jira/browse/CAMEL-23686
             Project: Camel
          Issue Type: Improvement
          Components: camel-core
            Reporter: Guillaume Nodet


h2. Context

Profiling Camel routes under high throughput (timer + splitter producing ~1M 
msg/s) reveals several memory and allocation inefficiencies in the Exchange 
lifecycle. This issue tracks concrete improvements identified through heap 
histogram analysis and JFR profiling.

h2. Findings

h3. 1. Dev profile forces messageHistory=true, overriding user properties 
(CRITICAL)

{{ProfileConfigurer.configureCommon()}} (line 102) unconditionally sets 
{{messageHistory=true}} in dev mode. This overrides any user setting of 
{{camel.main.messageHistory=false}} in application.properties, since the 
profile configurer runs after property loading.

*Impact:* With pooled exchanges in dev mode, {{DefaultMessageHistory}} 
instances accumulate unbounded — in our test, 2.95M instances consumed 236MB, 
ballooning heap from 75MB to 696MB. The history list grows because pooled 
exchanges recycle the exchange object but the debugger re-creates message 
history entries on each reuse.

h3. 2. Exchange pooling only covers consumer exchanges (~40%)

The {{PooledExchangeFactory}} only provides pooled exchanges for the consumer's 
initial exchange. Sub-exchanges created by Splitter, Multicast, and 
RecipientList use regular {{DefaultExchange}} instances.

In a pipeline route with a splitter, 524K out of 1.23M exchanges were pooled 
(42%). The remaining 703K (58%) were regular {{DefaultExchange}} instances, 
bypassing the pool entirely.

h3. 3. Per-exchange allocation is ~600 bytes across 10-12 objects

Each exchange allocates:
||Object||Bytes||
|DefaultExchange / DefaultPooledExchange|64-80|
|ExtendedExchangeExtension|80|
|EnumMap x2 (properties + internal)|80|
|DefaultMessage|48|
|CopyOnWriteHeadersMap|24|
|CaseInsensitiveMap|48|
|DefaultUnitOfWork|56|
|ReentrantLock + NonfairSync|48|
|ConcurrentLinkedDeque (routes)|24|
|*Total*|*~552 bytes*|

At 1M exchanges/s, this generates ~552MB/s allocation rate just for exchange 
infrastructure.

h3. 4. UnitOfWork is overweight for common single-route exchanges

* {{ConcurrentLinkedDeque<Route> routes}} — eagerly allocated, but typically 
holds only 1 entry. A simple field with lazy upgrade to deque would save 
allocation.
* {{ReentrantLock}} — allocated per UoW even though most exchanges are 
single-threaded. Could be lazily created only when threading is detected.

h3. 5. ExtendedExchangeExtension always allocated

{{ExtendedExchangeExtension}} (80 bytes) is created for every exchange, even 
though most exchanges never use extended features. Lazy initialization would 
save 80 bytes per exchange.

h2. Test Environment

* JDK: Temurin 21.0.9
* Camel: 4.21.0-SNAPSHOT (with PR #23738 CoW headers + PR #23766 O(1) 
CaseInsensitiveMap)
* Route: Timer(period=1) -> Split(1000 tokens) -> Direct -> CBR -> Direct -> 
mock
* Duration: 60s, heap histogram captured at 35s

h2. Benchmark Results

||Route||Profile||Heap Used||Metaspace||Threads||
|Baseline|prod + pooled|75 MB|42 MB|34|
|Baseline|dev + pooled|696 MB|43 MB|35|
|Pipeline|dev default|3,194 MB|45 MB|35|
|Pipeline|prod + pooled|1,270 MB|45 MB|34|
|HTTP|prod + pooled|95 MB|56 MB|58|

h2. Positive findings

* PR #23738 (CopyOnWriteHeadersMap) is working correctly — header copies are 
avoided
* PR #23766 (O(1) CaseInsensitiveMap) is active — TreeMap entries seen in 
histograms are from JMX infrastructure, not headers
* HTTP component adds a fixed 24 threads + 14MB metaspace (per-component, not 
per-exchange)



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to