Federico Mariani created CAMEL-24682:
----------------------------------------
Summary: camel-groovy - Resolve the language and
GroovyShellFactory once, keep the compiled script per expression, build the
binding lazily
Key: CAMEL-24682
URL: https://issues.apache.org/jira/browse/CAMEL-24682
Project: Camel
Issue Type: Improvement
Components: camel-groovy
Affects Versions: 4.22.0
Reporter: Federico Mariani
Every evaluation of a groovy expression does work that only needs doing once:
* {{GroovyExpression.instantiateScript}} calls {{resolveLanguage("groovy")}}
and {{registry.findByType(GroovyShellFactory.class)}} on every exchange
(GroovyExpression.java:70-71). {{findByType}} scans every bean in the registry,
so with 500 unrelated beans one evaluation is 11.8x slower (3.36 M -> 0.28 M
ops/s).
* The binding is rebuilt on every evaluation: a globals map, a copy of it,
{{ExchangeHelper.populateVariableMap}} (which copies all exchange properties
through {{getAllProperties()}}), a {{DefaultAttachmentMessage}} and a
{{Binding}}. That is ~154 ns and 1,152 B of a ~297 ns evaluation and 78% of the
bytes it allocates.
* The compiled script class goes through the shared 1,000-entry language cache
on every evaluation; a script evicted from it is recompiled (0.70 ms and 480 KB
per evaluation past 1,000 distinct scripts; a 5-minute load with 1,500 distinct
scripts ran at 5,019 msg/s with 600,000 classes loaded).
* The {{Script}} instance is created through {{ObjectHelper.newInstance}},
which copies the {{Constructor}} on every call.
Measured on main (JDK 21): {{body + '-' + header.foo}} at 3.0 M ops/s against
47.8 M for a Java expression; the route {{filter(amount > 100) ->
setBody(concat)}} at 0.48x the Java route on one thread and 0.22x on eight; a
5-minute 8-thread load at 1.08 M msg/s (0.26x Java) with a p99 of 102 us.
Proposed fix (implemented and benchmarked): resolve the language and the shell
factory once per CamelContext; keep the compiled class in the {{Expression}}
(checked against context, language and a generation counter bumped on language
stop and dev-mode reload, so the shared cache no longer removes classes on
eviction); a lazy {{ExchangeBinding}} that snapshots {{in}}, {{body}},
{{headers}}, {{exception}}, {{out}} and materializes {{exchangeProperties}},
{{variables}} and {{attachments}} on first access; a cached constructor
{{MethodHandle}}.
With the fix: expressions 4.9-7.7x faster, route 0.94x the Java route on 1 and
8 threads, load 3.80 M msg/s (0.91x Java) with a p99 of 3 us and the same 26 MB
heap after GC, 1,500 distinct scripts 2.92 M msg/s.
Behaviour changes that need an upgrade guide entry: a {{GroovyShellFactory}}
bound to the registry after the first evaluation is no longer picked up;
{{exchangeProperties}}, {{variables}} and {{attachments}} are read on first
access rather than when the binding is created. Protected {{createBinding}} /
{{instantiateScript}} signatures are kept.
Related: CAMEL-8259 (origin of the per-evaluation factory lookup), CAMEL-21212,
CAMEL-21717, CAMEL-21755 (binding variables).
----
_Found during a benchmark review of camel-groovy and the other scripting
languages (camel-performance-tests branch camel-groovy-perf,
tests/camel-jmh/groovy-results, JMH 1.37, JDK 21, one M1 Pro laptop). A
candidate fix with tests exists on the local apache/camel branch
camel-perf-all-fixes; a PR follows._
_Claude Code on behalf of Croway_
--
This message was sent by Atlassian Jira
(v8.20.10#820010)