Problem
ScalaFeelEngine.evaluateSimpleUnaryTests() and evaluateSimpleExpression() call feelEngine.evalUnaryTests(expression, context) / feelEngine.evalExpression(expression, context) on every invocation. These methods internally call parseUnaryTests() / parseExpression() each time, which includes:
- Full fastparse grammar parsing of the FEEL expression (recursive descent, AST node allocation)
- FeelParser.translateEscapes() — 8 calls to String.replaceAll(), each compiling a java.util.regex.Pattern from scratch (Pattern.compile → Pattern. → Pattern.expr → Pattern.sequence etc.)
- ExpressionValidator.validateExpression() — full AST traversal for validation
Since DMN decision tables have a fixed set of FEEL expressions in their rules, this means the same expressions are parsed hundreds/thousands of times per second under load — for no reason.
Impact
CPU flamegraph from production shows FeelParser.parseUnaryTests → translateEscapes → String.replaceAll → Pattern.compile as a significant CPU hotspot. For a DMN table with 10 rules and 3 input columns, each evaluateDecision call triggers 30+ full FEEL parse cycles.
Root cause
ScalaFeelEngine (in camunda-engine-feel-scala) uses the deprecated FeelEngine.evalUnaryTests(String, Context) / evalExpression(String, Context) API which always parses. The FeelEngine class already exposes parseUnaryTests(), parseExpression() and eval(ParsedExpression, Context) separately, but
ScalaFeelEngine does not use them.
Additionally, FeelParser.translateEscapes() (https://github.com/camunda/feel-scala/blob/main/src/main/scala/org/camunda/feel/impl/parser/FeelParser.scala) uses String.replaceAll() which compiles regex on every call. These patterns are static and should be pre-compiled.
Suggested fix
Option A (ideal): Add a ConcurrentHashMap<String, ParsedExpression> cache inside ScalaFeelEngine. On evaluateSimpleUnaryTests / evaluateSimpleExpression:
- computeIfAbsent(expression, feelEngine::parseUnaryTests)
- Then call feelEngine.eval(parsedExpression, context) with the cached AST
Option B (minimal): Pre-compile the 8 regex patterns in FeelParser.translateEscapes() as static Pattern constants instead of using String.replaceAll().
Environment
- camunda-engine-dmn: 7.24.0
- feel-engine: 1.21.0
Problem
ScalaFeelEngine.evaluateSimpleUnaryTests() and evaluateSimpleExpression() call feelEngine.evalUnaryTests(expression, context) / feelEngine.evalExpression(expression, context) on every invocation. These methods internally call parseUnaryTests() / parseExpression() each time, which includes:
Since DMN decision tables have a fixed set of FEEL expressions in their rules, this means the same expressions are parsed hundreds/thousands of times per second under load — for no reason.
Impact
CPU flamegraph from production shows FeelParser.parseUnaryTests → translateEscapes → String.replaceAll → Pattern.compile as a significant CPU hotspot. For a DMN table with 10 rules and 3 input columns, each evaluateDecision call triggers 30+ full FEEL parse cycles.
Root cause
ScalaFeelEngine (in camunda-engine-feel-scala) uses the deprecated FeelEngine.evalUnaryTests(String, Context) / evalExpression(String, Context) API which always parses. The FeelEngine class already exposes parseUnaryTests(), parseExpression() and eval(ParsedExpression, Context) separately, but
ScalaFeelEngine does not use them.
Additionally, FeelParser.translateEscapes() (https://github.com/camunda/feel-scala/blob/main/src/main/scala/org/camunda/feel/impl/parser/FeelParser.scala) uses String.replaceAll() which compiles regex on every call. These patterns are static and should be pre-compiled.
Suggested fix
Option A (ideal): Add a ConcurrentHashMap<String, ParsedExpression> cache inside ScalaFeelEngine. On evaluateSimpleUnaryTests / evaluateSimpleExpression:
Option B (minimal): Pre-compile the 8 regex patterns in FeelParser.translateEscapes() as static Pattern constants instead of using String.replaceAll().
Environment