Skip to content

FEEL expression parsing is repeated on every evaluateDecision call — no caching of parsed expressions in ScalaFeelEngine #1149

Description

@pe4enko

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:

  1. Full fastparse grammar parsing of the FEEL expression (recursive descent, AST node allocation)
  2. FeelParser.translateEscapes() — 8 calls to String.replaceAll(), each compiling a java.util.regex.Pattern from scratch (Pattern.compile → Pattern. → Pattern.expr → Pattern.sequence etc.)
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions