Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
src/EventSubscriber/GenericEventSubscriber.php simply performs a load() on the config entity storage to load reaction rules by event. That could be very expensive if there are many rules configs, because all of them have to be loaded.
Proposed solution: create a cache that holds all reaction rule config IDs per event.
Comments
Comment #1
fagoComment #2
klausiWIP at https://github.com/fago/rules/pull/428
Comment #3
fagohm, not so sure how the WIP is related?
I think we need to add some code in \Drupal\rules\EventSubscriber\GenericEventSubscriber::onRulesEvent() which takes the build $component object and caches that. Ideally, that code would be re-usable such that we can cache the $component of component configs with as well.
Comment #4
klausiOh? I thought we want to cache the config entity IDs to avoid doing a wildcard config entity load in the event subscriber.
We should probably do some profiling here to ensure that the caching has a significant effect.
Comment #5
fagoNope, I think this is already fixed in alex' branch - there actually is some nice key value store in place which allows us to query on config efficiently. I think we just need to cache the generated $component object so we do not have to do all the building up of the object based on multiple config objects.
Comment #6
klausiWhat $component object? You mean the expression object that is created from a reaction rule config?
Comment #7
fagoNo, I mean the component object as provided via RulesUiComponentProviderInterface. This is the central object used by UI + execution, which does not depend on config. But the component used during event execution will be different, as it should add all reaction rules of the event to it. Right now we execute the expressions manually in a state, but if we put it into a component we should be able to cache it nicely, i.e. using the same way for component configs? -> see \Drupal\rules\EventSubscriber\GenericEventSubscriber::onRulesEvent()
Comment #8
fagoMaybe we should add an EventSet expression for that? It worked like that in d7 also, i.e. it created an event set object and cached it.
Comment #9
klausiHm, for executing reaction rules we don't need a rules component? That would be just a lot of setup, while we currently just execute the expressions, which is faster? What could we cache in that regard?
In GenericEventSubscriber we do:
How would RulesComponent help with that?
Or do you mean creating a new class EventExpressionExecutor that holds the expression objects? It would not be an expression object, because it is just a container for expressions.
Comment #10
fagoNo, I meant something like an ActionSet - this does nothing more than looping over expressions and executing them either but can hold all that code related to logging or cloning state. I don't see any harm in doing an expression plugin for that?
>How would RulesComponent help with that?
Good question. Setting up the state is something that might be good to cache, but then this should be straight-forward and cached by the EventManager. So I guess what it would end-up caching mostly is the created expression objects, so creating the expression objects from configuration arrays would be cached - instead we've a serialized tree of objects which is ready to execute. Then, of course - one cache hit is better than many ;)
Comment #11
fagoBut yeah, we definitely should start benchmarking this - e.g. make a page request which runs a rule and fire ab on it. Or just run it a PHP-loop, but that would benefit from static-caches in an unrealistic way.
Comment #12
klausiDid some profiling of the current 8.x-3.x branch.
Generating dummy Rules with this script (warning: deletes all your Rules!):
Then adding the following to settings.php to disable render caching so that the rules fire on every node page request:
Create a node so that there is a page that we can make requests to.
Then run the generate scripts with different values for $nr_rules.
To get some numbers I use Apache bench:
That sends 20 requests in serial to Drupal.
The median response time is then Percentage of the requests served within 50% of requests.
Results on my laptop with this:
Median:
1 rule: 392ms
10 rules: 412ms
100 rules: 491ms
1000 rules: 1572ms
Note that generating those 1000 rules already takes quite a while :-D
Comment #13
klausiMeh, I had Xdebug enabled - disabled that and repeated the test:
1 rule: 272ms
10 rules: 274ms
100 rules: 318ms
1000 rules: 1014ms
But really the most surprising number is when I uninstall Rules: 92ms
Not sure what is going on there - probably some cache kicking in that I have overlooked.
Comment #14
klausiTested again with this hook implementation added to user module that does the same thing as the show message action in the rule:
Median for that is 248ms, which sounds more realistic.
Rules adds a performance penalty of 10% when enabled with one rule compared to a developer implementing the same functionality in a hook.
Comment #15
fagoI got everything working so far, but it doesn't work because serialziation of rule objects is broken. I'll fix that in a separate issue (separate problem), so I think this is ready for a first code review.
https://github.com/fago/rules/pull/447
The architecture allows modules implementing different config-storage to add their own component resolvers (e.g. for storing rules in content entities). Also, the RulesComponentRepository which handles the caching logic is a service which could be swapped by a module / future PR that wants to implement a PHP-code execution strategy. So the approach should be future-safe.
Comment #16
klausilooks good and tests are passing except for coding standards, setting to needs work for that.
we should also repeat the profiling to check how much has improved.
Comment #17
fagoThanks, I corrected the coding style issues. Could you help me with the profiling and re-run your test so we see what changed?
Comment #18
fagook I repeated the tests:
Test (without xdebug):
ab -n 20 http://drupal-8.localhost/node/1
Without rules:
Time per request: 164.178 [ms] (mean)
Without the patch:
0 rules: Time per request: 164.495 [ms] (mean)
1 rule: Time per request: 172.297 [ms] (mean)
10 rules: Time per request: 175.394 [ms] (mean)
100 rules: Time per request: 220.783 [ms] (mean)
1000 rules: Time per request: 699.044 [ms] (mean)
With the patch:
0 rules: Time per request: 155.587 [ms] (mean)
1 rule: Time per request: 161.042 [ms] (mean)
10 rules: Time per request: 163.648 [ms] (mean)
100 rules: Time per request: 190.642 [ms] (mean)
1000 rules: Time per request: 580.814 [ms] (mean)
Generally, the tests had high varying results when I repeated them (+-30ms!). I just tried to ran them ~10 times each and picked the lowest numbers I got. Generally I was able to get lower numbers with the patch applied though. Else we see executing the Rules is quite performance costly and would need more profiling & improvement.
Comment #20
fagook, merged it.