Proposed commit message:
Issue #2498919 by stefan.r, Berdir, catch, plach: Node::isPublished() and Node:getOwnerId() are expensive
Problem/Motivation
Node::isPublished() and Node::getOwnerId() are accessed during node access checks. This goes through TypedData which ends up taking up to 20ms when profiling with xhprof on my machine.
Proposed resolution
Set these as entity keys on the node - berdir pointed out he's already doing this for user's on #2345611-55: Load user entity in Cookie AuthenticationProvider instead of using manual queries
We could maybe have two properties:
$this->entityKeys would contain the non-translatable keys only
$this->translatableEntityKeys would contain the translatable keys only
On a node this gives:
entityKeys: bundle, id, revision, uuid
translatableEntityKeys: label, langcode, status, uid, default_langcode
We profile to confirm the saving. We access TypedData from elsewhere sometimes (for example comment links), so in those cases it may not end up a net improvement. However, nodes with no comment fields should be a good test case.
Remaining tasks
User interface changes
None.
API changes
None.
Comment | File | Size | Author |
---|---|---|---|
#78 | interdiff-75-76.txt | 689 bytes | stefan.r |
#78 | 2498919-76.patch | 16.76 KB | stefan.r |
#76 | 2498919-75.patch | 17.53 KB | stefan.r |
#69 | 2498919-69.patch | 16.76 KB | stefan.r |
#69 | interdiff-62-69.txt | 3.63 KB | stefan.r |
Comments
Comment #1
catchHere's a patch and xhprof screenshots.
This will be more of a saving when combined with #2498849: Entity view controller title rendering is expensive which further cuts down TypedData access.
Comment #2
catchNow comparing:
HEAD + #2498849: Entity view controller title rendering is expensive
HEAD + #2498849: Entity view controller title rendering is expensive + #1.
I profiled:
Standard profile.
Disable page cache
Create 'page' node.
Log out.
Profile node/2.
With both patches combined, this removes every call to ConfigEntityBase::get(), saves between 10-16ms and about 1mb of memory (according to xhprof on my machine).
Comment #6
stefan.r CreditAttribution: stefan.r commentedPerhaps a stupid question but as entity keys aren't translatable, how could we mark a specific translation as published/unpublished if the status is an entity key?
Or is @Berdir working on that in the other issue?
Comment #7
dawehnerI see we load those in the beginning of
core/lib/Drupal/Core/Entity/ContentEntityBase.php:166
, so what about not loading them on pages with multilingual content?Comment #8
BerdirGood point. I don't think not loading them is an option, I doubt that's actually possible and it prevents it from working for non-translatable keys as well.
Maybe we need to key $this->entityKeys with the langcode.
Comment #10
dawehnerSo an ContentEntity object is never constructed with more than one langcode, right? Are we sure that we can't load it, as you expect it, in the right language?
Comment #12
BerdirYes it is. A contentEntity always has all the langcodes and translation values and one of them is active.
Comment #13
stefan.r CreditAttribution: stefan.r commentedSo could we do $this->entityKeys[$langcode][$key_name] instead of $this->entityKeys[$key_name] and fill up all the langcodes that have a value? And then in getEntityKey we use the active langcode?
Comment #14
BerdirYeah, something like that is what I was thinking. A bit of overhead for id/type/uuid that aren't actually translatable, but that's tricky to identify without causing additional overhead
Comment #15
stefan.r CreditAttribution: stefan.r commentedOK, just posting a rudimentary/untested patch for feedback and so we can see what this would look like.
Comment #16
dawehnerShould we not set those special keys to all languages, so we don't trigger
->get()
for them?Comment #17
stefan.r CreditAttribution: stefan.r commentedYes, this currently does some unnecessary gets as we only set the language-specific values that are defined in $this->values. But maybe we can think of a cleaner way than just copying the value from the default language to all other possible languages?
There's no way for us to distinguish between translatable/untranslatable keys without too much overhead either?
@Berdir what do you think?
Comment #18
BerdirWas thinking about exactly that. We could maybe have two properties, $this->entityKeys and $this->entityKeysPerLanguage. We could check the first and the second and if neither has it, add it to the right one based on the field definition isTranslatable() information?
Comment #19
stefan.r CreditAttribution: stefan.r commentedI had thought of something similar yes.
$this->entityKeys
would contain the non-translatable keys only and$this->entityKeysPerLanguage
the translatable keys only?Comment #20
BerdirYes, exactly.
Comment #21
stefan.r CreditAttribution: stefan.r commentedJust posting another unfinished (broken) patch for feedback. Maybe one of you can easily see what's wrong here (it's ignoring me when I try to unpublish a node).
On a node this gives:
entityKeys: bundle, id, revision, uuid
entityKeysTranslatable: label, langcode, status, uid, default_langcode
Comment #22
stefan.r CreditAttribution: stefan.r commentedActually this shouldn't be green as the following didn't work with this patch:
- enable content translation
- add a language
- add a translatable content type
- translate a node to another language
- unpublish the translation through the node edit form
---> translation is still published
So we probably need to add a test for this.
Comment #23
dawehnerComment #24
stefan.r CreditAttribution: stefan.r commentedComment #26
stefan.r CreditAttribution: stefan.r commentedJust posting another patch here that should to illustrate the problem.
Under certain circumstances, when we unset the
$node->entityKeysTranslated
property in theonChange
method triggered during publication of the node (i.e. when we do$this->parent->onChange($this->name)
inItemList::setValue()
during the ::publish handler), this change is not being propagated to the entity object in the node form, soisPublished
will still get the "outdated" value in the "status" entity key when the field has already been updated, resulting in the translation not being saved correctly. I could only reproduce this on a translatable content type without custom fields, with a body and a title only, when adding/editing a translation.This may be pointing to an underlying bug...
Comment #27
dawehnerI can't tell where, but it feels like there would be a great place somewhere to add some documentation, why we are doing that: performance optimization for some cases ...
Comment #28
dawehnerLike somewhere here maybe, see previous commet ...
Comment #29
stefan.r CreditAttribution: stefan.r commentedI asked @plach to have a look at the onChange issue.
Also:
Comment #30
YesCT CreditAttribution: YesCT commentedupdating summary
Comment #31
tstoecklerI recently saw the following in
ContentEntityBase::initializeTranslation()
:So either that code does not actually work or we are thinking too complicated here :-)
Comment #32
plachThis fixes the issue reported in #22.
We already have
NodeTranslationUITest::doTestPublishedStatus()
that's supposed to test this, we should update that one instead.Comment #33
stefan.r CreditAttribution: stefan.r commentedThanks, that fix makes sense. I think we can remove the snippet in this interdiff as well then.
Comment #34
stefan.r CreditAttribution: stefan.r commentedShould we really be hardcoding the label property here, what if we have more language-dependent properties than just the label?
Maybe we can rename this to something like #set_published_status to prevent confusion?
Comment #36
stefan.r CreditAttribution: stefan.r commentedComment #37
stefan.r CreditAttribution: stefan.r commentedComment #39
stefan.r CreditAttribution: stefan.r commentedActually$this->activeLanguage
shouldn't change in the lifetime of an object, meaning there should be no need to put the translatable values on a separate$entityKeysTranslatable
variable, we can just store whatever the value for the active language is.Comment #41
Berdir$this->activeLanguage has nothing to do with the current language of the site or the container. It definitely does change when you call getTranslation() (what happens is that the object is cloned and $this->activeLanguage is changed.
But @tsteockler has a good point.. we need to either unset them all in __clone() or key by active langcode, not both. Not quite sure yet what's better, keying by language might be better, because it's quit likely that sites will load the entity and then switch the language, this improvement won't work well on multilingual sites otherwise.
Comment #42
stefan.r CreditAttribution: stefan.r commentedOK let's go back to the previous approach then, I must've misunderstood on IRC :)
Comment #43
stefan.r CreditAttribution: stefan.r commentedComment #47
stefan.r CreditAttribution: stefan.r commentedComment #49
stefan.r CreditAttribution: stefan.r commentedComment #51
stefan.r CreditAttribution: stefan.r commentedComment #53
stefan.r CreditAttribution: stefan.r commentedThat was an attempt at doing away with the isTranslatable check for performance reasons but I think it'd be cleaner if we had this information somehow, this patch feels a bit hacky.
Comment #55
stefan.r CreditAttribution: stefan.r commentedComment #56
plachNot sure what I was supposed to validate, I reviewed the whole patch, just in case. It looks good to me, the only things that I found are:
We can swap these two tests if we want improve performance a bit.
Not sure why this was changed from
#published
to#set_published_status
: it doesn't look like an a DX improvement to me. What about just#published_status
?Btw, it would be nice if someone could add me to the contributors list, since I wrote parts of the patch :)
Comment #57
BerdirThe owner is an entity reference field, so this won't work for them.
We need a second case here for target_id.
We can use elseif here, for a tiny performance improvement, since we know that we don't have both.. I think?
A bit confused by the logic here.. did the plan with two different properties not work? Then we wouldn't have to hardcode bundle here.
I have a feeling that the current code isn't really working for id/revision ID and so on.
Comment #58
BerdirWorking a bit on this.
1. actually works for owner because it's a base field and not inside 'value'.
Confirmed that on multilingual sites, we still have to get the field definitions to access the value, even for things like the id. That's not so nice and a bit a regression.
So let's try something like this, only did manual tests.
With this patch, I can *almost* render a node (without comments) without getting the field definitions, the only thing that breaks it is NodeViewBuilder::getBuildDefaults(), the check for in_preview. Wondering if we can solve that in a better way.
#2507455: TypedData::setValue() with $notify set to TRUE on a get request causes unnecessary onChange notification is also relevant, because currently, setting _attributes on e.g. the title unsets the entity key and it needs to be refetched. Doesn't make a big difference really because it only happens on a render cache miss which means that we had to instantiate the title field anyway to render it.
i don't like that there's so much code in the constructor, I'm thinking about a helper method or something there to avoid duplicating it.
Comment #60
BerdirNo idea how that test works right now, but the current default language in the language manager is en, not de. Rebuilding solves that. Also fixed a bug in setDefaultLanguage() that cased one of the two test fails there.
That might not be 100% correct yet for untranslatable entity types with a langcode field.
Comment #61
BerdirComment #62
BerdirBy documenting is_preview as a public property, which is not nice but just reflects reality, we can avoid the __isset() and, at least without comment module, view node/N without having to load the field definitions at all.
Also did some profiling with a normal authenticated user (user 1 is not useful as that one bypasses the access checks), with standard but without comment fields to get the best possible improvement.
Total wall time varies too much for me (I'm seeing anything between 66-120ms), but we're saving 800 function calls (2.4%) then and 1.2MB memory (7%).
Node::access() is 2.2ms (57%) faster, no calls to typed data manager or getFieldDefinitions() left. That also means that we load quite a bunch of classes less.
Comment #63
stefan.r CreditAttribution: stefan.r commented@Berdir I had gotten rid of the two different properties in an attempt to have less calls to isTranslatable() and not having to load field definitions as suggested earlier by catch. But it doesn't seem like something we can easily get around, so #62 makes more sense than #55
@plach as to #published vs #set_published_status, I got confused myself when reading the code, I assumed "published" referred to the current state. If it's not an improvement let's put it back to whatever it was and just put a comment to clarify. Or maybe instead of a boolean it could be a called 'publish' or 'unpublish'.
That doesn't seem right :)
Do we still need to hardcode this?
Comment #65
catchHere's what I get profiling:
It's an even more dramatic change when profiling the history module's AJAX request to mark a node as read.
Comment #66
stefan.r CreditAttribution: stefan.r commentedJust out of interest where does the 14 ms increase in Wall time come from in that first run?
Comment #67
BerdirPretty sure that's just the normal variation, in my case, I've seen anything between 66ms and 120ms with both patches, I just picked two that were close together.
If you want to get better results, you could try to set up and run https://github.com/LionsAd/xhprof-kit, that's going to do lots of requests and then it takes the average to compare it.
Comment #68
stefan.r CreditAttribution: stefan.r commentedYes, when I do 100 runs the Wall time decreases rather than increases. On a regular node page:
http://www.lionsad.de/xhprof-kit/xhprof/xhprof_html/?run1=5589b6f75b296&...
Comment #69
stefan.r CreditAttribution: stefan.r commentedThis already has test coverage, right?
Comment #70
Fabianx CreditAttribution: Fabianx as a volunteer commentedLooks good.
Comment #71
Fabianx CreditAttribution: Fabianx as a volunteer commentedProposed commit message:
Issue #2498919 by stefan.r, Berdir, catch, plach: Node::isPublished() and Node:getOwnerId() are expensive
Comment #74
stefan.r CreditAttribution: stefan.r commentedComment #75
Fabianx CreditAttribution: Fabianx as a volunteer commentedDoes this need a change record, btw.?
Comment #76
stefan.r CreditAttribution: stefan.r commentedre-roll
Comment #77
Fabianx CreditAttribution: Fabianx as a volunteer commentedRe-roll looks great!
Comment #78
stefan.r CreditAttribution: stefan.r commentedWell the translatableEntityKeys property just an added internal property and it doesn't affect the actual API, so I would assume not.
Don't know if we need one for the added entity keys defined on the node either (uid, status) as those only ought to be accessed through isPublished() / getOwnerId().
A little change crept into the re-roll, sorry.
Comment #79
Fabianx CreditAttribution: Fabianx as a volunteer commentedDoh, yes :)
Comment #80
catchCommitted/pushed to 8.0.x, thanks!
Comment #82
catchComment #84
nlisgo CreditAttribution: nlisgo commentedI'm posting here so that those involved in this issue might be able to lend a hand in resolving what appears to be a regression in the node preview feature as a result of this fix.
#2535792: Preview feature broken if requested twice
If any of you who contributed here could take a quick glance at the problem and it might be apparent to one of you what the solution might be.
Comment #85
jhedstromThis should probably have had an upgrade path since it changed the schema on uid and status fields. Discovered this over in #2507201: Upgrade path for MySQL switch to multibyte UTF8 which adds node and term data to the starting db for update tests.