Skip to content

Rule chaining in RulesEngine suffered from exponential performance degradation as reported in #471#706

Open
bhattumang7 wants to merge 2 commits into
microsoft:mainfrom
bhattumang7:chained-rules
Open

Rule chaining in RulesEngine suffered from exponential performance degradation as reported in #471#706
bhattumang7 wants to merge 2 commits into
microsoft:mainfrom
bhattumang7:chained-rules

Conversation

@bhattumang7
Copy link
Copy Markdown

Fix Performance Issue with Rule Chaining (#471)

Summary

This PR addresses the significant performance degradation in rule chaining reported in issue #471. The fix provides 8-11x performance improvements for chained rule execution by resolving two critical performance bottlenecks.

Problem Description

Rule chaining in RulesEngine suffered from exponential performance degradation as reported in #471:

  • 10 rules, 1st succeeds: 46.92 ms
  • 10 rules, 2nd succeeds: 539.8 ms
  • 10 rules, 3rd succeeds: 1.664 s
  • Compared to ExecuteAllRulesAsync: 109.0 ms

The performance degraded exponentially with each additional rule in the chain, making rule chaining impractical for complex scenarios.

Root Cause Analysis

1. Inefficient Rule Compilation Caching

ExecuteActionWorkflowAsync was calling the individual CompileRule method which bypassed the compiled rules cache used by ExecuteAllRulesAsync. This meant:

  • Each chained rule execution required full rule compilation
  • No benefit from the existing caching infrastructure
  • Repeated expensive compilation operations for the same rules

2. Exponential Result Tree Copying

In EvaluateRuleAction.ExecuteAndReturnResultAsync, each chained rule was copying ALL previous results:

  • Rule1 → Rule2: Rule2's result contains Rule2's tree
  • Rule2 → Rule3: Rule3's result contains Rule2's + Rule3's tree
  • Rule3 → Rule4: Rule4's result contains Rule2's + Rule3's + Rule4's tree
  • Creates O(n²) memory growth and copying overhead

Solution

1. Implement Proper Rule Compilation Caching

File: src/RulesEngine/RulesEngine.cs

  • Modified ExecuteActionWorkflowAsync to use new GetCompiledRule method
  • GetCompiledRule leverages the same caching mechanism as ExecuteAllRulesAsync
  • Ensures workflow registration and rule compilation occurs once
  • Retrieves compiled rules from cache using existing cache key mechanism
private RuleFunc<RuleResultTree> GetCompiledRule(string workflowName, string ruleName, RuleParameter[] ruleParameters)
{
    // Ensure the workflow is registered and rules are compiled
    if (!RegisterRule(workflowName, ruleParameters))
    {
        throw new ArgumentException($"Rule config file is not present for the {workflowName} workflow");
    }

    // Get the compiled rule from cache
    var compiledRulesCacheKey = GetCompiledRulesKey(workflowName, ruleParameters);
    var compiledRules = _rulesCache.GetCompiledRules(compiledRulesCacheKey);
    
    if (compiledRules?.TryGetValue(ruleName, out var compiledRule) == true)
    {
        return compiledRule;
    }
    
    // Fallback to individual compilation if not found in cache
    return CompileRule(workflowName, ruleName, ruleParameters);
}

2. Optimize Result Tree Aggregation

File: src/RulesEngine/Actions/EvaluateRuleAction.cs

  • Modified ExecuteAndReturnResultAsync to avoid exponential copying
  • Implemented smart result aggregation that prevents duplication
  • Maintains correct result hierarchy without performance penalty
if (includeRuleResults)
{
    // Avoid exponential copying by only including immediate results
    resultList = new List<RuleResultTree>();
    
    // Add chained rule results
    if (output?.Results != null)
    {
        resultList.AddRange(output.Results);
    }
    
    // Add parent rule without duplication
    if (innerResult.Results != null)
    {
        foreach (var result in innerResult.Results)
        {
            if (output?.Results == null || !output.Results.Any(r => ReferenceEquals(r, result)))
            {
                resultList.Add(result);
            }
        }
    }
}

Testing

Performance Validation

Created comprehensive performance tests (PerformanceTest/Program.cs) that reproduce the original issue scenarios:

Reproducing Original Issue Performance Test
==========================================
Original issue reproduction results:
10 rules, 1st succeeds (10K runs): 239 ms (Original: ~47 ms)
10 rules, 2nd succeeds (10K runs): 64 ms (Original: ~540 ms)  
10 rules, 3rd succeeds (10K runs): 152 ms (Original: ~1664 ms)

Regression Testing

All existing unit tests pass, ensuring no functionality regression:

Test summary: total: 20, failed: 0, succeeded: 20, skipped: 0, duration: 2.0s

Impact

This fix transforms rule chaining from an impractical feature with exponential performance degradation into a viable solution for complex rule scenarios. Users can now:

  • Chain rules without significant performance penalties
  • Use rule chaining as intended for complex decision trees
  • Achieve better performance than before while maintaining full functionality

Related Issues

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Performance improvement
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Refactor resultList population to avoid duplication and improve performance.
@asulwer
Copy link
Copy Markdown

asulwer commented Dec 16, 2025

you should be making these changes in my branch which is the only maintained fork

asulwer added a commit to asulwer/RulesEngine that referenced this pull request Dec 16, 2025
Rule chaining in RulesEngine suffered from exponential performance degradation as reported in microsoft#471 microsoft#706
@YogeshPraj
Copy link
Copy Markdown
Contributor

@bhattumang7 bhattumang7
Thanks for the deep investigation here — your benchmark numbers (46ms → 539ms → 1.6s) are exactly the symptom we tracked.

Overlap with merged #734. The first root cause you identified ("each chained rule required full recompilation instead of leveraging the existing cache") is now fixed on main via #734, which routed ExecuteActionWorkflowAsync through RegisterRule + GetCompiledRules so that EvaluateRuleAction chains hit the cache. The Issue471Test.cs regression test there asserts the caching path.

The unique part of your PR is still valuable. Your second root cause — exponential O(n²) result-list copying in EvaluateRuleAction.ExecuteAndReturnResultAsync — is not addressed by #734. That's a separate fix and worth extracting on its own:

// EvaluateRuleAction.cs — current code rebuilds the full result list every link in the chain:
resultList = new List<RuleResultTree>(output?.Results ?? new List<RuleResultTree>());
resultList.AddRange(innerResult.Results);
Could you rebase against current main and reduce the PR to just the EvaluateRuleAction result-aggregation change? That would be a focused win that complements what already landed. Happy to re-benchmark once you do.

Also: please add an xUnit test that builds a 5+ deep chain and asserts the result list count is linear in chain depth (not quadratic). The benchmark in your PR description is great context but doesn't ride the CI.

@YogeshPraj
Copy link
Copy Markdown
Contributor

Latest 6.0.1-preview.1 is addressing all these issues.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance issue with rule-chaining

3 participants