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)

Reply via email to