You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#707 reported a large slowdown after upgrading past 5.0.x. It was closed via #727 (compiled-delegate cache) and the 6.0.1-preview releases, but the underlying root cause is still present: every expression parse triggers a full AppDomain assembly scan. The cache from #727 only helps when the same expression string recurs; for workflows whose rules are unique expressions (the normal case), compilation is still ~6x slower than 5.0.3.
Root cause
Commit 71d59dc ("[Bug Fix] Handling of Automatic Type Registration", #675) changed CustomTypeProvider.GetCustomTypes() to include base.GetCustomTypes():
publicoverrideHashSet<Type>GetCustomTypes(){varall=newHashSet<Type>(base.GetCustomTypes());// <-- scans all AppDomain assembliesall.UnionWith(_types);returnall;}
DefaultDynamicLinqCustomTypeProvider.GetCustomTypes() scans every loaded assembly for [DynamicLinqType] types. System.Linq.Dynamic.Core caches that scan per provider instance — but RuleExpressionParser.Parse constructs a newParsingConfig and CustomTypeProvider for every expression parsed:
Dynamic LINQ's KeywordsHelper enumerates GetCustomTypes() on every ExpressionParser construction, so the assembly scan runs once per expression — and rules with LocalParams parse several expressions each. Before 71d59dc, GetCustomTypes() returned only the explicitly registered types, so no scan occurred at all; that is why 5.0.x did not exhibit this. (This also explains the "lots of first chance exceptions, dynamic assembly types" observation in #707 — the scan swallows ReflectionTypeLoadException per assembly, per parse.)
Benchmark
20,000 rules, each with 2 LocalParams and a unique main expression, one registered custom type, 174 assemblies loaded in the AppDomain (to approximate a real service). Timing the first ExecuteAllRulesAsync call (which compiles the workflow), net9.0, Release:
Package
Warmup
RulesEngine 5.0.3
17.3 s
RulesEngine 5.0.6
16.4 s
RulesEngine 6.0.1-preview.2
113.8 s (6.6x)
6.0.1-preview.2 + fix below
16.3 s
The regression scales with the number of loaded assemblies, so larger services see worse ratios.
Fix
Two small changes restore 5.0.3-level warmup with no behavior change (all 170 unit tests pass):
RuleExpressionParser: build the ParsingConfig/CustomTypeProvider once and reuse it across parses, rebuilding only when ReSettings.CustomTypes is swapped (which AutoRegisterInputType does on workflow registration). This preserves the intent of [Bug Fix] Handling of Automatic Type Registration #675 — the scan results and registered input types are still honored — while running the scan once instead of once per expression.
CustomTypeProvider: memoize the merged type set — it is fixed after construction but was being rebuilt (including the base scan) on every GetCustomTypes() call.
I have this ready as a PR, plus a follow-up PR that adds opt-in parallel rule compilation (ReSettings.EnableParallelRuleCompilation, default false) which brings the 20k-rule warmup down further from 16.3 s to 4.7 s on a 16-thread machine.
Summary
#707 reported a large slowdown after upgrading past 5.0.x. It was closed via #727 (compiled-delegate cache) and the 6.0.1-preview releases, but the underlying root cause is still present: every expression parse triggers a full AppDomain assembly scan. The cache from #727 only helps when the same expression string recurs; for workflows whose rules are unique expressions (the normal case), compilation is still ~6x slower than 5.0.3.
Root cause
Commit 71d59dc ("[Bug Fix] Handling of Automatic Type Registration", #675) changed
CustomTypeProvider.GetCustomTypes()to includebase.GetCustomTypes():DefaultDynamicLinqCustomTypeProvider.GetCustomTypes()scans every loaded assembly for[DynamicLinqType]types. System.Linq.Dynamic.Core caches that scan per provider instance — butRuleExpressionParser.Parseconstructs a newParsingConfigandCustomTypeProviderfor every expression parsed:Dynamic LINQ's
KeywordsHelperenumeratesGetCustomTypes()on everyExpressionParserconstruction, so the assembly scan runs once per expression — and rules withLocalParamsparse several expressions each. Before 71d59dc,GetCustomTypes()returned only the explicitly registered types, so no scan occurred at all; that is why 5.0.x did not exhibit this. (This also explains the "lots of first chance exceptions, dynamic assembly types" observation in #707 — the scan swallowsReflectionTypeLoadExceptionper assembly, per parse.)Benchmark
20,000 rules, each with 2
LocalParamsand a unique main expression, one registered custom type, 174 assemblies loaded in the AppDomain (to approximate a real service). Timing the firstExecuteAllRulesAsynccall (which compiles the workflow), net9.0, Release:The regression scales with the number of loaded assemblies, so larger services see worse ratios.
Fix
Two small changes restore 5.0.3-level warmup with no behavior change (all 170 unit tests pass):
RuleExpressionParser: build theParsingConfig/CustomTypeProvideronce and reuse it across parses, rebuilding only whenReSettings.CustomTypesis swapped (whichAutoRegisterInputTypedoes on workflow registration). This preserves the intent of [Bug Fix] Handling of Automatic Type Registration #675 — the scan results and registered input types are still honored — while running the scan once instead of once per expression.CustomTypeProvider: memoize the merged type set — it is fixed after construction but was being rebuilt (including the base scan) on everyGetCustomTypes()call.I have this ready as a PR, plus a follow-up PR that adds opt-in parallel rule compilation (
ReSettings.EnableParallelRuleCompilation, default false) which brings the 20k-rule warmup down further from 16.3 s to 4.7 s on a 16-thread machine.https://github.com/microsoft/RulesEngine/pull/740/changes
https://github.com/microsoft/RulesEngine/pull/741/changes