Skip to content

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

Merged
asulwer merged 6 commits intomainfrom
performance-loss
Dec 16, 2025
Merged

Rule chaining in RulesEngine suffered from exponential performance degradation as reported in #471 #706#158
asulwer merged 6 commits intomainfrom
performance-loss

Conversation

@asulwer
Copy link
Owner

@asulwer asulwer commented Dec 16, 2025

PR Classification

This pull request delivers a bug fix and performance improvement for rule result handling, along with dependency updates and documentation enhancements.

PR Summary

This update refines rule result aggregation to prevent duplication and improves rule compilation performance, while also updating dependencies and documentation.

  • EvaluateRuleAction.cs: Refactored result aggregation logic to avoid exponential copying and ensure only unique, immediate results are included when chaining rules.
  • RulesEngine.cs: Added GetCompiledRule method to optimize retrieval of compiled rules from cache, enhancing workflow execution performance.
  • CHANGELOG.md: Added detailed entries for versions 6.0.13 and 6.0.12, including new features and bug fixes.
  • RulesEngine.csproj, DemoApp.csproj, RulesEngine.UnitTest.csproj: Upgraded multiple NuGet package dependencies and incremented the library version to 6.0.14.

@asulwer
Copy link
Owner Author

asulwer commented Dec 16, 2025

credit for this PR goes to @bhattumang7

Fix Performance Issue with Rule Chaining (microsoft#471)

Summary

This PR addresses the significant performance degradation in rule chaining reported in issue microsoft#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 microsoft#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 senarios.

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)

@asulwer asulwer self-assigned this Dec 16, 2025
@asulwer asulwer requested review from RenanCarlosPereira and removed request for RenanCarlosPereira December 16, 2025 17:34
@asulwer asulwer merged commit 31a77d3 into main Dec 16, 2025
3 checks passed
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.

1 participant