Problem/Motivation
comment_entity_storage_load() attempts to optimize away a database query each time entities are loaded that don't have comment fields attached.
Unfortunately when caches are cold - such as after submitting the modules page, the field check takes 125ms and 4mb of memory.
Proposed resolution
Query statistics just-in-time and in bulk to avoid needing the loading etc and the field checks.
Remaining tasks
Uploading a patch for #1, but I think we should do #2 (which will be slightly more work).
User interface changes
API changes
New service
New data-types
Comments
Comment #1
fabianx commentedLets go for #2, statistics don't belong on the object / field itself, entities should really be immutable.
Comment #2
catchComment #3
catchHere's a rough patch for #2, unfortunately not as straightforward as it could be.
The main issue is CommentLinksBuilder() runs inside hook_node_links_alter(), and hook_node_links_alter() runs inside a post render cache callback, and the entity that is passed in there is a fresh one from Node::load(), not the one in the view builder that has run through hook_entity_prepare_view() - so we have to ensure that data gets fetched again.
Similar issue with the field formatter - again the entity in the field items doesn't have this information set on it.
Uploading the patch to see if these are the only issues though.
It'd be better not to modify the entity at all - instead inside CommentStatistics we can have a method that given an entity, gives you back the statistics - and that can have a static cache/class property that's populated in hook_entity_prepare_view() to avoid fetching stuff every time. This is a bigger change though.
Comment #4
fabianx commentedCould always add a static cache to comment_prepare_statistics for now ...
Comment #8
yesct commentedComment #9
larowlanSo originally we had a 'calculated' statistics field.
What if we did this:
So essentially, lazy loading - we keep track of the items that might need to be read, but we don't read them until we actually use it?
Or is that basically going to end up with the same thing in terms of memory use?
Comment #10
catch#9 should be fine for memory as long as we avoid the fields check - it's this on a cold cache that is particularly bad. I think this means:
- markOnRead() in load
- CommentStatisticsInterface::read() multi-loads and caches only for one entity type at a time (i.e. the entity type that stats are being read for).
That means both no fields check and no entity types being unnecessarily queried.
When reading #9 I initially thought 'why not do markAsRead in entity prepare view' - but this runs into a similar problem to #3. When lists of entities are render cached, prepare view won't run, which means postRenderCache would end up individually querying statistics for each entity. As long as every entity is loaded before the post render cache callbacks are called, then we'll still get multi-load from statistics (this may not be the case though - entities are individually loaded in the post render callback so it assumes them being loaded before that, might not be the case).
Comment #11
andypostLooks like duplicate of @catch's issue
Comment #12
catchSomething is wrong when you open duplicates of your own issues...
Marked the older issue as duplicate since this has more up-to-date activity.
Comment #13
andypostAlso there's #2318875: Redo CommentStatisticsInterface but not sure that fits into beta stage.
The primary problem with comment stats that we need
comment_countin comment, forum, and rdf modules as entity property mostly always before build/render happens.Comment #14
fabianx commented#13: Well, usually hopefully during render stage, not before.
Also a simple:
$comment->getStatistics() should be enough for that case, which is pre-loaded in entity_prepare_view().
Changing entity properties after loading that are not part of the schema is highly problematic and leads to different problems in regards to cacheability and future placeholdering, so if we can avoid it: Great!
Comment #15
andypost@Fabianx not
$comment->getStatistics()but$commented_entity! SO no way to add this method to any entity that have comment field attachedComment #16
larowlannext on my radar
Comment #17
larowlanFirst pass
So instead of checking for a comment-field on the given entity-type, we just keep track of all content entities that pass through comment_entity_storage_load in a new service (CommentStatisticsLazyLoaderInterface).
We then convert the CommentItem->{various} statistics columns into their own data types.
When ->getValue is called on any of these, we load the statistics (in bulk) just-in-time.
This means it is harmless to mark an entity that is loaded as needing statistics (because the statistics will never be requested).
I think this gives us the best of both worlds - we retain the bulk loading, retain the 'no querying for entities that don't have comments' and lose the expensive field checks
Also, if the statistics aren't even used on the page, no loading happens at all (which isn't the case in HEAD).
Needs tests for some unit tests of the new service.
Comment #19
larowlanSo in some instances node view builder is being invoked before the node is loaded? how does that work? render cache contexts?
Comment #23
berdirWe need to redo that profiling.
That information is from before the field map was rewritten to be persisted, we do not calculated that anymore like we used to, we just load it from key value, should be much, much faster.
There might still be reasons to do this, but I'm fairly sure that we should change this to a normal issue or maybe even close it.
Comment #30
hchonovRe #23:
This is not the only problem, therefore I would not close the issue.
I was chasing performance problems with our application and I've noticed some strange behavior. Consider an entity type with a comment field.
Execute the following command considering there are 20000 entities in the system.
Now because of the disabled garbage collection for cyclic references and the initialization of a field on entity load the memory consumption will be raising on each entity load, which consequently might/will exhaust the allowed memory size.
Even with the garbage collection for cyclic references enabled it is not a good idea to execute unneeded queries, especially when a large amount of entities are being loaded one after another and not at once.
---
We have a comment field, but we don't need the statistics at all and would actually turn them completely off. Loading the statistics lazy only when they are needed seems to be the best solution for me.
Comment #31
geaseA plain reroll against 8.9.x. The only meaningful difference (except moving some lines up and down) was removing of condition
in comment_entity_storage_load() that was fixed in #2527866: Investigate why comment.manager service doesn't exist when comment_entity_storage_load() is fired in ConfigImportAllTest
Comment #32
andypostthe number of failures is less then previous patch
Comment #33
geaseDid a little fixing for tests. The only failing unit test, CommentStatisticsUnitTest, had to be updated because CommentStatistics::read() now returns associative array. To fix CommentItem kernel test, small change was made to CommentStatisticsLazyLoader, obviously, the possibility of missing statistics was not accounted for completely.
Comment #34
hchonovWhen we access a property we go through the magic methods.
$node->comment_field->comment_countshould trigger-> \Drupal\Core\Field\FieldItemList::__get()-> \Drupal\Core\Field\FieldItemBase::__get()However if we haven't accessed the property yet through
\Drupal\Core\Field\FieldItemBase::get($property_name)which is implemented on the parent -\Drupal\Core\TypedData\Plugin\DataType\Map::get(), then the property will not be initialized yet and therefore\Drupal\Core\Field\FieldItemBase::__get()will returnNULL.The problem is that
\Drupal\Core\Field\FieldItemList::__get()calls the magic method\Drupal\Core\Field\FieldItemBase::__get()and not the method that actually initializes the properties -\Drupal\Core\Field\FieldItemBase::get($property_name)and the reason for that is that it also returns values that are not field properties as documented in the method's body.I think that the problem here lies within
\Drupal\Core\Field\FieldItemBase::__get():According to this code it will work only if the property has been initialized already, which will not be the case until the
::get()method is called.Maybe we should try loading the property if it is not yet initialized:
If I am correct then
Drupal\Tests\comment\Functional\CommentStatisticsTestshould not fail anymore after that or at least the assertions which fail because of returned valueNULLshould pass.Comment #35
andypostThank you for digging it, it reminds me about why we override list class for comment field, because default values sometimes missing
Comment #36
andypostComment #37
andypostComment #38
geaseApproach in #34 seems promising, but it needs a bit of refinement. We step ahead of checking value logic with $this->get($name), which may have unexpected side effects. In fact, implementing this approach exactly as suggested didn't fix the test and broke my local installation immediately with the message
I will try to move things around in FieldItemBase::_get() to achieve desired result.
Meanwhile, I tried to initiate properties in comment_entity_storage_load(), and it worked to the extent I've seen. Let's see what tests report. But as we are chasing the speed here, I'm not sure if this solution fits 100%.
Also new version of patch contains one kernel test fix.
Comment #39
geaseHere's implementation of approach from #34. As my superficial testing showed, there was no bad in trying to call $this->get($name) in FieldItemBase::__get() in last place. We will get a meaningful exception if we try to address a non-existing property instead of NULL, which is even better. I'm giving this patch a number of 38b instead of 39 to show that it's not a consecutive development, but a different approach.
Comment #40
geaseSeems there were too many exceptions with non-existing properties. So adding a check if property exists for the field. Also fixed code style.
Comment #41
geaseThere is a number of changes to the patch in previous comment.
First, we are introducing clearValues method for CommentStatisticsLazyLoader to flush the statically cached statistics.
Then, we are introducing a LazyLoadableInterface that should be implemented by lazy-loadable typed data and use it for stricter check on if we should try to load values for non-instantiated properties in FieldItemBase::__get().
Third, in CommentItem::getValue() we needed to instantiate properties to get behavior consistent with the current one.
And some tests were fixed (kernel test by adding filter module - though it is not needed with the introduction of p.2), functional test by using the above clearValues method, because resetting node storage cache wouldn't be enough to retrieve the actual data.
Comment #42
geaseIn this update,
Comment #44
cspitzlayreroll with adaptation to typo fix
Comment #51
catchPretty sure this is still valid per #30, the check is done in the wrong place, but I think this is more of a task so switching over.