Now we have done some more work (part4) and this part 5 that is coming up in the next 3.7 LTS release.
This time we have mainly been focusing on reducing the footprint of Camel after bootstrapping.
separating design time model vs runtime processors
We have continued the modularisation and have separated the design time vs runtime parts in the core.
The route model is now in camel-core-model, and the runtime EIP implementations are in camel-core-processor. And in between we have camel-core-reifier which transforms the model into processors. This separation plays a significant part of continued effort of making Camel even smaller, such as Quarkus and GraalVM runtimes. This will essentially allow us to pre build from the route model the runtime processors, and then at runtime not even include the model and reifier classes. There is still some work to get to the finish line, but Camel 3.7 is a major step forward.
For runtimes that are not Quarkus or GraalVM based, then we have a new lightweight switch in camel-main / CamelContext that can be turned on, that does some internal optimizations by null'ing the model and reifiers from the runtime processors, which then aids the JVM garbage collector, so it can reduce memory.
The following two screenshots shows the camel-example-main-tiny running with Camel 3.4.4 and 3.7.0 (lightweight mode).
The top screenshots shows that there are 731 Camel objects and that they take up about 58kb of memory in the heap. Now in Camel 3.7 this has been significant improved as there are only 340 objects and they take up 25kb. This means Camel has shrunk to half the size in memory footprint. The memory referred to here is the memory used by Camel to bootstrap and start itself.
Improved type converter
The type converter system have been optimized to be quicker for common convertions, and as well reducing the number of method calls during a conversion. The core converters are now bulked together and source code generated into 1 class per maven artefacts (2 in total for core). Before this there was about 10 different converter classes. This helps reduce the footprint of the type converter registry significantly. Now all the core converters are referenced by just 2 classes. Before each converter method was registered via a double keyed map which leads to a map containing 200 converters (takes up 20kb of heap memory). And on top of that then each converter method is invoked via lambda call that the JVM compiles into a separate class (so there was also 100 extra classes loaded). So all together this greatly reduces the footprint by lowering the heap memory usage by 20kb and 100 less classes loaded. And if you are using any of the XML components then camel-core-jaxb/camel-core-jaxp comes with 100 converters as well, which means if they were loaded then that would double the memory footprint. And the type converter is now also faster as the lookup of the converter is quicker and uses just Java primitives, where as before new objects was created as keys to lookup in the internal registry map. So there is also less garbage for GC.
The following screenshot shows the same example as before with Camel 3.7.0 (no lightweight mode):
Here we can see that there are 346 objects and they take up 28kb of memory. That is only +6 objects and +3kb more memory. So the lightweight mode did not in this example reduce as much memory. That is because there is only 1 route and 4 different models in use. If the application has more routes and more EIP models and reifiers, then more memory would be reduced.
The cost is however that the application cannot dynamically add new routes (via the model). The lightweight mode should be used with caution and it is only for "static Camel applications".
Optimized direct component
The direct component has been optimized to wire up its producers and consumers during startup. Before the direct producer would for each message then lookup its associated consumer before sending the message to the consumer. We have moved this to an earlier phase. And then in case a consumer is stopped / removed / or re-created etc then the producer is capable of invalidating its old wiring and re-associate to the new consumer. This improved avoids internal lookup and thread synchronisation.
Optimized event notifier
Another performance improvement was to divide event notifiers into lifecycle vs messaging events. As lifecycle events such as staritng and stopping Camel does not affect what happens during routing messages. We use this knowledge to avoid checking for runtime events, in the routing engine, if there isn't any custom lifecycle added to Camel.
Parsing without regular expressions
Some parts in Camel uses regular expressions to parse, such as a string to long time converter, so you could convert 10m30s (as 10 minutes and 30 seconds). This was using several regular expressions. However when they get compiled by JVM they objects in memory are rather large. There are other places internal in Camel that regular expression was used for more basic parsing. So we rewrote those using plain basic Java and this reduced the memory and has better performance. As using regular expression in the first place was a bit overkill.
Naming in IT is hard, so we named a new marker interface BootstrapCloseable, after java.io.Closeable. We use this to mark services and other entities in Camel as something that are only used during bootstrapping Camel. Then after Camel has been started, we will then invoke those so they can free up resources and memory.
More to come
There will be more to come for Camel 3.7. But at this point we wanted to share the good news that Camel is on a diet and have shrunk to half the size for its memory footprint (startup).
We have plans to look at a compiled simple language which would greatly improve performance for some simple expressions that are dynamic and use method calls. One area of inspiration is the new camel-joor language which gets compiled to Java during bootstrap.