Suggested commit message
Issue #2273277 by Wim Leers, effulgentsia, Fabianx: Fixed Figure out a solution for the problematic interaction between the render system and the theme system when using #pre_render.
Problem/Motivation
#2099131: Use #pre_render pattern for entity render caching has had to introduce hacks to get cache tags to bubble up correctly. In a nutshell, it's because the theme system/Twig invokes drupal_render() on a copy of the render array, then executes #pre_render callbacks on that copy, and hence cache tags and #post_render_cache metadata doesn't bubble up as it should — causing repeated cache gets and crazy hacks — hacks that we cannot expect contrib developers to replicate.
However, this is not just a clean-up issue
, this really is a critical bug, because:
- Contrib developers forgetting (or not knowing) to apply these hacks would cause cache invalidation to break, thus serving stale content.
- The way developers have been building render arrays for years, is broken, depending on what you are rendering (i.e. if somewhere in what you're rendering something uses a
#pre_rendercallback that sets a#post_render_cachecallback for a small uncacheable part, it won't work: that#post_render_cachecallback will never be executed). This patch makes it work as expected. - The current work-around causes render cache checking to happen twice in some cases! For example, an entity reference field that is configured to use the "Rendered entity" formatter, will first perform a
drupal_render()callback in the formatter itself (this is the hack necessary to get that rendered entity's cache tags to bubble up) — this causes a render cache item to be created. Then, when the entity that contains the entity reference field is actually being rendered, we'll check the render cache again (as part of the regular/actual rendering flow). That's a cache write and a cache read, which should only have been a write.
See #7 for an explanation.
Proposed resolution
- Use a stack-based approach to bubbling up bubbleable rendering metadata — see #36 for an explanation.
- Remove all existing hacks.
Remaining tasks
Review.
User interface changes
None.
API changes
None.
Original report by @Wim Leers
See #2099131-177: Use #pre_render pattern for entity render caching, below the first HR and #2099131-186: Use #pre_render pattern for entity render caching for detailed explanations.
I've also encountered this while working on #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees and #2254865: toolbar_pre_render() runs on every page and is responsible for ~15ms/17000 function calls.
I've been chatting with effulgentsia about this — I hope he will chime in also.
(I will update this issue as soon as I find the time.)
This issue has been encountered in at least the following issues:
- #2099131: Use #pre_render pattern for entity render caching
- #2217877: Text filters should be able to add #attached, #post_render_cache, and cache tags
Suggested commit message:
Issue #2273277 by Wim Leers, effulgentsia, Fabianx: Fixed Figure out a solution for the problematic interaction between the render system and the theme system when using #pre_render.
| Comment | File | Size | Author |
|---|---|---|---|
| #47 | problematic_interaction_stack_solution-2273277-47.patch | 125.39 KB | wim leers |
| #23 | xhprof runs.zip | 334.9 KB | wim leers |
Comments
Comment #1
catchI don't have an idea on how to fix this properly yet, not without a massive refactor of both systems.
One thing we discussed as far back as Prague, but I don't think has an issue. FabianX brought it up originally. It's a temporary hack but seems like it should work OK.
In some cases where we call drupal_render(), there's a proper place to take the #attached and #cache information out and send it upwards.
In some cases it just gets lost, these are always nested calls to drupal_render() within a parent drupal_render() call (either direct or via _theme() or something else).
For the latter, we could introduce a temporary static inside drupal_render(), and populate it with #attached/#cache information (probably only when $is_recursive_call is true). Then when drupal_render() is called $is_recursive_call = false, reset that static because we can assume the #attached/#cache is getting passed up properly from that point onwards.
Comment #2
effulgentsia commentedYes, I think we could even make it a stack, and always push/pop it regardless of how drupal_render() is invoked. I think we could also create a Render service (leave drupal_render() as a BC wrapper to it), so as to make the static into an instance variable, and then have a method for retrieving the current value from the stack in places where we need that.
Also, I wonder if we want to move all things that need to bubble into #attached. In other words,
#cache['tags']to#attached['cache_tags']and#post_render_cacheto#attached['post_render_cache']. That way, we only need a single #attached collector instead of the multiple collectors we have now.Comment #3
catchOn the second point have you seen #2239457: Move main complexity of drupal_render() into Drupal\Core\Render\Render? I haven't reviewed it yet.
#attached['cache_tags'] could work, I wonder whether it would be confusing since that has previously been reserved for page-level assets, but worth looking at yes.
Comment #4
wim leersComment #5
fabianx commentedIt is possible to put things there temporarily, but especially cache tags can't be drupal_array_merge_deep()'ed and I think post_render_cache would work.
I am using that approach in D7:
drupalRender()
@see http://cgit.drupalcode.org/render_cache/tree/plugins/render_cache/contro...
and
getRecursionStorage()
@see http://cgit.drupalcode.org/render_cache/tree/plugins/render_cache/contro...
and to store data in #attached:
http://cgit.drupalcode.org/render_cache/tree/plugins/render_cache/contro...
It should be way simpler in core though as all goes via drupal_render() anyway ...
Comment #6
rainbowarrayJust a note that Wim Leers and catch discussed this in IRC yesterday. Wim said he was working on a possible solution. Hopefully there's a good way to tackle this!
Comment #7
wim leersI started looking at this, because we're seeing this problem in the menu render caching patch (#1805054: Cache localized, access filtered, URL resolved, and rendered menu trees), in the "messages as a block" patch (#2289917: Convert "messages" page element into blocks) and will quite likely affect #507488: Convert page elements (local tasks, actions) into blocks as well.
I updated the issue title and summary to reflect a very important part of the problem, that wasn't very explicitly present before: the problems described in this issue happen only because Drupal 8 leverages the
#pre_renderpattern much more than Drupal 7! In Drupal 7, it was only used for minor things. In Drupal 8, it's used for very big things, e.g. for blocks and entities, to allow us to do the very least amount of work possible on a cache hit.Consequently, in Drupal 8, the render array (tree) for the page is relatively sparse: it doesn't contain all the information yet. In Drupal 7, it contained everything.
Combine that with the fact that whatever is passed to the theme layer (
theme()in D7,_theme()in D8) gets passed by value, which means that#pre_rendercallbacks invoked bydrupal_render()calls made in the theme layer will not affect the original (root) render array that represents the entire page. Consequently, any cache tags, assets or#post_render_cachecallbacks added in those#pre_rendercallbacks will not be available to the "root"drupal_render()call (the one that renders the entire page), and therefore those cache tags, assets and#post_render_cachecallbacks will be "lost". (Well, assets will work fine due to an implementation detail: they still use a global static, but we've worked to get rid of that).And finally, in Drupal 7, we had (almost?) no render caching in Drupal core, and only rarely used
#pre_renderfor simple things, so we simply didn't notice this problem.Therefore, the solution to the problem is actually quite simple: we must make sure that the "page-wide" render array is the full render array, i.e. with all
#pre_rendercallbacks executed.This patch does that.
In writing this patch, I also:
#defaults_loaded, so it's possible that defaults are loaded twice. Previously, any render array was only touched once, now it may be touched twice (first for executing#pre_rendercallbacks to ensure it's fully built, the second time for actual rendering).Comment #8
wim leersComment #9
wim leersComment #11
wim leersI wonder why the test-only patch was not reviewed by testbot? Testbot--
Uploaded the same "test-only" patch, and fixed the actual patch.
Comment #12
fabianx commented#11: Hiding the patch file on initial POST does not get the file tested, only files added to the top files list get tested by the testbot. This is by design (as far as I know).
To the patch itself. I don't fancy too much going through the whole render_array twice, but it is a possible solution for now.
Also need to think about a #post_render_cache having other post render cache data attached to it, for e.g. placeholders:
A block being a placeholder containing a node with another placeholder.
Comment #13
wim leers#12:
RE: hiding patch file on initial post: oh… HAH! That'd explain it. Attaching the test-only patch just once more then, without hiding it. Thank you very much for the tip!
RE: to the patch itself: I don't like it much either (in general, there's a lot to be disliked about
drupal_render()), but it's the only solution I can think of.RE: : That will work just fine;
#post_render_cachecallbacks performing rendering usedrupal_render($build), notdrupal_render($build, TRUE)(which must be used for recursive calls, e.g.drupal_render()does this itself internally), which means you'll get a fully rendered result back, which includes performing any#post_render_cachecallbacks.Comment #15
wim leersSorry for the noise here. Re-uploading the test-only patch that will fail (as shown in #13) and the actual patch up for review that will pass (as shown in #11). Renumbering them to remove all confusion, but they're effectively unchanged from above.
This should leave the issue at the "needs review" status, because, yes, this is now blocked on your reviews!
Comment #17
wim leersSigh, does the order of those patches matter also? This is definitely NR, not NW.
Comment #18
rainbowarrayI think you typically want to put the patch that will fail first, followed by the patch that should pass.
I'm not sure that I can comment much on the internal workings of this patch, but excited to see progress!
Comment #19
damiankloip commentedHad a quick glance.
can use the new element_info service directly.
Is this an optimisation or trying to get around something you don't want the controller resolver to do? Could do with a comment either way on that I think.
This does not seem necessary. Could leave the if as-is and do nothing to $callable otherwise
Comment #20
wim leers#19: everything you reviewed is copy/pasted existing code in
drupal_render(). So I'm afraid all three points are out of scope… Sorry!Comment #21
damiankloip commentedBut you are copying and pasting it into a new function. The fact that you got it from drupal_render () does not seem like a good reason to have it wrong in two places.
The scope of the issue is the function you are adding?
Comment #22
wim leers#21: Fine, fixed #19.1 and #19.3 — those are simple enough changes: it's clear that changing those won't have consequences. Fixing #19.2 can easily have consequences though, so please see #2323301: Deprecate the "two double colons" check in Renderer::doCallback() for that.
Having addressed #19/#21, let's now get back to architectural reviews.
The main concern I had with the patch in #15 is that it duplicated logic, instead of moving logic. DRY and all that. So I spent some time cleaning that up. In this reroll:
drupal_render()no longer loads#typedefaults nor applies#pre_rendercallbacks. Those are tasks for_drupal_render_ensure_fully_built(), because those are the two crucial things for "fully building a render array".drupal_render()now only handles the render cache HIT case for the non-recursive ("root") call todrupal_render(), because in this case, there is no need to fully build the render array, since the full render array has a cache hit. When the full render array has a cache hit, we must also apply all#post_render_cache callbacks. This is not the case for render cache hits for render cache hits at any child/descendant level of the render array. (We always only apply#post_render_cachecallbacks at the very latest moment possible, i.e. when the full render array has been built, which happens immediately when we have a render cache hit for the full render array.)Sadly, it's possible for theme preprocess functions to generate render arrays (or forms), which then won't have been accessible during the non-recursive ("root") call to
drupal_render(), and hence they will not be fully built. Hence we not only have to call_drupal_render_ensure_fully_built()during the non-recursive ("root") call todrupal_render(), but also if we see in recursive ("non-root") calls to it that either#defaults_loadedis not set or#pre_renderstill exists (_drupal_render_ensure_fully_built()would have taken care of that, if this render array had not been added in a theme preprocess function).(This happens in e.g. table rendering, View rendering, but likely also in other places.)
TextTrimmedFormatterhas now been completely removed; no more straydrupal_render()call there!WebTestBase::drupalBuildEntityView()has now been simplified: it simply calls_drupal_render_ensure_fully_built()rather than applying defaults and executing#pre_rendercallbacks itself. (Which was also implemented incorrectly, for it only did this at the "entity level", it did not recurse down to the "field level".)Comment #23
wim leersJust to make sure that this doesn't cause a performance regression, I did some profiling.
Setup:
Loading
node/1, best out of 10 runs:As you can see, essentially status quo. Slightly more function calls (which is only logical, given that we move part of what
drupal_render()used to do in a new helper function). Slightly faster. Slightly more memory (this must be because due to the changes in text summary rendering, because no additional work is being done). Overall, essentially the same performance.Comment #24
yched commentedTriple yay on the simplification of the formatters and the removal of the complex explanations !
Regarding the "trimmed" formatter :
- trimming moves to pre_render in order to avoid needless computation on cache hit, right ? Might be worth a comment ?
- could we add that callback as a static method on the formatter class rather than as a new procedural function ?
- nothing is done for the "summary_or_trimmed" formatter ?
Comment #25
wim leersThanks for the review yched!
Yes. But to be completely honest, I didn't do it for that purpose intentionally: the way the formatter worked today, it needed
#markupto contain the filtered text, because otherwisetext_summary()couldn't be applied to it. That meant I had to rundrupal_render()in a place where it didn't belong — regardless of performance considerations.But, of course, a really is just another way of saying "is an indication of needless computations on cache hits"/"is bad for performance".
I could add a comment, but I don't think it's necessary: any
drupal_render()invocations outside code building responses are a code smell. If you prefer, I don't mind adding that comment though :)Absolutely! Done.
Indeed, because there's nothing to do:
because
(Yes, that is confusing!)
Comment #26
wim leersWalked moshe through my patch, addressed his feedback in this reroll:
drupal_render(), so that it's now very clear what happens during the root call, and what happens in other cases.childrentofoo, to avoid any initial confusion by readingchildrenas#childrenrender()that was introduced in #2099131: Use #pre_render pattern for entity render cachingComment #27
effulgentsia commentedI don't quite follow the above comment:
$variables['author_picture'] = user_view($node->getOwner(), 'compact');. But I don't see that as something we can or want to remove: I think it's legitimate for preprocess functions to add new variables that are render arrays.{{ author_picture }}will still stop there, and not bubble up to the node's render array, since that's the point at which we only have a copy and not the original parent. Or am I missing something on how this patch makes that work? I don't see a test added that covers the condition of bubbling up stuff generated by the rendering of a Twig variable.Additionally, I'm somewhat concerned about this patch's approach of pre-rendering all descendants (to all depths) prior to invoking #theme on the ancestor, since the Twig template might not even want to print out some of its variables (or a preprocess might set #access of a particular child to FALSE). At which point, we've wasted time calling #pre_render functions of things that don't get rendered, thus losing some of the benefits of moving expensive code into them (we still retain the fast cache hit benefits, but lose out on the on-demand execution of cache miss benefits).
In other words, unless I'm wrong about the above, I'm wondering if rather than working around the lossy render <-> theme interaction by trying (but failing) to get the entire tree fully built ahead of the root call to theme(), if we should re-evaluate the idea in #1 of making that interaction not lossy.
Comment #28
wim leers#27:
Great questions! I'm afraid proper answers are long by necessity. The short version:
drupal_render()has no sense of hierarchy. #1 can only work if a recursive call knows/receives the state of the parent call, but because the recursive calls go through the theme system and hence (have to) operate on copies of render arrays, it is impossible to send hierarchy/state to child calls. Therefore, render array subtrees that have render caching enabled may get the wrong cache tags. The page will indeed receive the correct cache tags, but that would always be the case when using a static: both the page and the static are "request-wide".Besides the two above points that show this patch is complete, this does not break anything, so if #1 turns out to be possible after all, it can be done in a follow-up.
This issue is a hard blocker for the critical performance issue #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees, so it would be great to get that finally unblocked.
(Emphasis mine.)
Great catch! But AFAIK we decided a long time ago that we don't support this? We say we don't support attaching assets in theme preprocess functions. (Precisely because we cannot bubble attached assets up from preprocess functions; assets belong in the "content structure" (render) layer, not in the "printing" (theming) layer).
That is, we say that the render array must contain the full structure of the content. Because… if the application logic isn't aware of what is going to be rendered, then how could it possibly be held responsible for invalidating it correctly?
Note that we specifically want to get rid of the preprocess layer, which will also allow us to remove the cited code block: #2060783: Remove the preprocess layer..
And last but not least, I went through all
HOOK_preprocess_*functions and I only found a handful remaining ones that are problematic (all others have been fixed), which indicates how far we've come with eradicating this bad practice from the past. See [1] below.Setting
#accessdoes not belong in preprocess functions. That's once again application logic. If the render array does not contain the "truth" about which things are going to be rendered and which won't, then we will collect the wrong cache tags as well. Setting#accessbelongs in#pre_render/#process/alter hooks (e.g.file_managed_file_pre_render(),filter_process_format(),seven_form_node_form_alter()).Setting
#accessin a preprocess functions happens exactly zero timesin HEAD.Hence this is a non-issue.
#1 only offers a correct result for the overall page rendering: all assets, cache tags and
#post_render_cachecallbacks are collected in a global static, and that static applies to the page level. That's doable and easy. A static is an easy way to achieve that. But the picture becomes much bleaker when we take into account that parts of the page may be render cached as well, which is the case.Let's take a simple example:
Let's say for the sake of simplicity that Blocks aren't render cached nor have cache tags, only Node, Author (user entity) and Entity reference (another node entity) are render cached and have cache tags. The above tree representation is already sorted by #weight.
Let's call "the static"
S.Sis a set. Initially,Sis empty. (S = {}).Facts
drupal_render()will have$is_recursive === TRUEfor every element in the tree, except for thedrupal_render()call for the Page. This means thatis unfortunately wrong AFAICT.
drupal_render()call does not have any context/hierarchy information.drupal_render()renders elements in the tree in sequence, and depth-first.Render flow
S = { user:1 }.S = { user:1, term:1, term:2 }. This is still correct.Shere, the render cache item contains the correct information. But nowS = { user:1, term:1, term:2, user:2 }.S = { user:1, term:1, term:2, user:2, term:3 }.S = { user:1, term:1, term:2, user:2, term:3, node:1 }. ButScontains cache tags from the containing node! So the cache tags are WRONG!S = { user:1, term:1, term:2, user:2, term:3, node:1, node:4 }. Node contains the correct information, and bubbles up to Page.I think this sample flow shows how #1 cannot work, because the render system does not have any sense of place/hierarchy.
With #1, correct bubbling/metadata cannot be guaranteed for nested render cache items, only for the page-level render array.
[1]: the remaining
HOOK_preprocess_*()functions that either calldrupal_render()or add a new renderable array:template_preprocess_table()template_preprocess_comment()template_preprocess_file_widget_multiple()forum.module(unsurprisingly so, this module has gotten little love this cycle, just like other cycles)template_preprocess_node()template_preprocess_node_preview()And a few call
drupal_render()just to attach assets (which happens to still work because we still use a global static under the hood, but we've been working towards removing that):For assets/drupal_add_html_head*:
template_preprocess_maintenance_page()template_preprocess_book_navigation()seven_preprocess_install_page()seven_preprocess_maintenance_page()Comment #29
rainbowarrayAt first glance, this seems highly problematic.
My understanding is that removing the preprocess layer is off the table for d8.
If this effectively eliminates the possibility of adding render elements in preprocess, that is quite likely to cause problems. For one example, take a look at some of the issues I ran into with adding dynamic css/js calls that wouldn't work with a libraries.yml file: #2298551: Documentation updates adding/adjusting inline/external JS in themes. One of the few things that worked was adding a render element with #attached in preprocess.
I know the render "API" is a giant pain. But for themers, if they need to do something more complicated than what is possible in a template, the first place they go is preprocess. To tell them that render elements are verboten in preprocess seems like it's going to cause huge headaches.
I'll be honest, I don't understand all the internals of how this works, but as somebody who primarily does front end work, this approach sounds concerning to me.
Comment #30
wim leers#29: This patch does not change anything WRT preprocessing. It just doesn't fix it (it is already broken in HEAD from a bubbling POV). You can still render things just fine in the preprocess layer (HEAD does that too), but with one big caveat: cache tags and
#post_render_cachecallbacks will not work. If I'm right in #28, there is literally no way around that. But that means as long as you're adding fairly simple things in preprocess functions (that don't need either cache tags or#post_render_cachecallbacks), you'll be fine. Note that I've explicitly omitted assets: because assets still use a global static internally, hence attaching assets from preprocess functions continues to work as long as D8 keeps that implementation detail.Comment #31
rainbowarrayOkay. Well I'm all for us getting this issue is in so I can get on with the work I've been doing on page variable to menu block conversions. It sounds like this is possibly set to go. Who needs to review this to get us to RTBC?
Comment #32
effulgentsia commentedI'm currently discussing this with Wim to work out the implications of #28.
Comment #33
fabianx commentedI still put on the table that a recursive approach for collecting out-of-band data works - it even works with hacking drupal_add_js / drupal_add_css in D7 and processing by the same recursive-tracking drupal_render function.
Just to need a huge render tree up-front might have other implications and while it works, the user_view() in preprocess is a really good example.
Do we really want to go with one giant array that we deep-process to the end?
Why use #pre_render at all when we only cache at certain points anyway. Why not RenderCacheControllers or such that know how to render the objects contained, but also support proper cache tags, etc support and are able to know the render chain?
Comment #34
fabianx commentedContradicting myself, but this issue might make #953034: [meta] Themes improperly check renderable arrays when determining visibility easier ...
And while cache tags are important to track and as long as we support preprocess we should somehow get them back to the main array - cause now we definitely know its an out-of-band render, that could be follow-up ...
e.g.
preprocess:
$author = user_view();
=> cache tags are wrong cause node teaser would not be invalidated when author changes.
Comment #35
wim leersShort update: @effulgentsia is AWESOME! We've been chatting all afternoon, and we've (well, mostly he) figured out how to make a stack-based approach work. Most crucial tests are passing already. We'll post here when we have a working patch!
Comment #36
wim leersAn alternative solution: the stack-based approach
I'm providing a condensed version of the conversation I had with @effulgentsia.
I asked effulgentsia how the stack would then yield correct results in the following example:
To which he answered:
I prototyped it… and it worked!
The approach I've used in all patches so far has one big advantage: debuggability. It becomes much easier to debug render arrays, because the render array is fully built at the very beginning, rather than only a single level at a time, and potentially on a copy of a render array in a template.
However, there are three even bigger advantages to the stack-based approach:
#post_render_cachecallbacks even from the theme preprocess layer. So those kinds of one-offs will work just fine. Of course it still won't be recommended, because the structure should live in the render array/application layer, not in the theme layer.#pre_rendercallback)! This is an improvement compared to HEAD, because HEAD will associate all cache tags, even if something is not actually printed. After all, if the markup for something isn't present, then we shouldn't invalidate a page when that something changes!drupal_render_collect_attached()drupal_render_collect_post_render_cache()drupal_render_collect_cache_tags()… and it so happens that these were responsible for a lot of function calls, recursing down trees to collect needed data. Especially the one for
#post_render_cachewas expensive, because it even had to do render cache gets for correct results. All of that complexity is now *gone*!Concrete example of bubbling from the theme layer
Let me give one concrete example. HEAD currently does this in
template_preprocess_node():The cache tags for this rendered user entity were never bubbled before. But now they are!
Important consequence #attached bubbling fixed!
Also quite important: because the bubbling of attached assets is now working correctly, #2168111: Allow drupal_render() to pass up #attached to parents is in fact fixed. To prove that it's indeed fixed, this patch should (and does) remove the call to
drupal_process_attached()indrupal_render().Why? Because that call caused everything to seem to "work just fine" for assets, thanks to the global statics in
_drupal_add_css()and_drupal_add_js()(which are called bydrupal_process_attached()). But it actually causes assets to "load just fine" on cache misses (because of the global statics), but utterly fail on cache hits (because the code that caused the global statics to be updated wouldn't run, precisely because it's a cache hit).Removing
drupal_process_attached(),_drupal_add_js()and_drupal_add_css()in this patch would be a huge scope expansion, so we still calldrupal_process_attached()… but only after entire the entire render array has been rendered! I.e. inDefaultHtmlFragmentRendererand friends. This is a huge step forward for asset handling, is something we've been working towards for more than a year, and makes #1762204: Introduce Assetic compatibility layer for core's internal handling of assets much more doable.This patch versus previous patches
This patch uses the same test coverage as added by previous patches, it only changes the solution (completely). The key changes live in
common.inc, just as before.Comment #37
rainbowarrayWell this sounds amazing. How fast can we get this to RTBC? :)
Comment #38
moshe weitzman commentedGenius! I wonder if this will also take some pressure off of the Views cache tag issues. Might those early renders just work now?
I read through very carefully and found nothing to complain about. It is obvious that you polished this well before uploading. Thanks!
I think we need docs for the stack stuff in drupal_render() and then this is RTBC.
Comment #39
effulgentsia commentedThanks for the nice words in comments like #35 :)
I also want to make sure to leave a public comment (for posterity) here acknowledging @Fabianx for coming up with the idea initially (see #1) and working on a variant of the idea (see #5).
And of course, a HUGE thank you to Wim for turning the idea into a patch that passes tests!
Comment #40
fabianx commentedThanks Wim! This is great!
I am glad a stack-based recursive solution was possible in the end!
I will review this in more depth probably next week - unless it has been committed by then already though :).
From a first look, it looks great!
Thanks, #39 for the nice words and thanks for making it a reality with Wim :)
Comment #41
wim leersI updated
drupal_render()'s docs as requested in #38.In doing so, I found one small bug: if a
#pre_rendercallback added bubbleable metadata (e.g. cache tags) and set#printed = TRUE, then the bubbling wouldn't have worked. There is nothing in core that does this, and it just makes the handling of bubbleable rendering metadata in the "early return if#printed, abort rendering" case exactly the same as in the "render cache HIT, abort rendering" case, so I don't think additional test coverage is necessary. This small change only applies the same pattern consistently.#37, #38: thank you for the kind words! :) But as effulgentsia says, this also wouldn't have possible without the initial idea of Fabianx!
Suggested commit message:
Issue #2273277 by Wim Leers, effulgentsia, Fabianx: Fixed Figure out a solution for the problematic interaction between the render system and the theme system when using #pre_render.Comment #43
fabianx commentedComment #46
fabianx commentedNeeds a re-roll, I reviewed the patch too AND the usage of SPLStack is awesome!
Really nice nice work, and one of the best documented patches I have ever seen, RTBC in my book.
Comment #47
wim leersChasing HEAD.
Now profiled the stack-based solution. Results are in line with the expectations: more function calls (for updating the stack at every point along the way, but these are simple, fast function calls), similar memory consumption, but the more complex/the bigger the render array, the bigger the CPU time improvement.
Tested with PHP 5.5.11, OpCache, 5 article nodes with only their title and body fields filled in. All tested with the root user (UID 1), so includes rendering of the toolbar.
#post_render_cache/admin/config/development/testing(the most expensive/slowest to render page we have in Drupal core)Conclusion: a small performance improvement for simple pages, a big improvement for complex pages. This patch simplifies rendering, and hence makes it faster.
We can still try to optimize the performance of the stack bubbling in the future further (and hopefully improve performance further), but the current patch is good to go from a performance POV.
#46: very glad to read that! :)
Comment #48
rainbowarrayAm I reading this right that there is a 23% performance improvement on a complex page? If so, that's amazing.
Comment #49
wim leers#48: That's correct! But this is a very complex page, you won't see such big improvements on page loads. Nevertheless, nice consequence of simplification :)
Comment #50
effulgentsia commentedRTBC per #46.
Comment #51
joelpittetNice work @Wim Leers! Love to see performance stats:D
Just an FYI about that testing page. We had a quite large performance regression when converting the table to #type => table. #1938926-29: Convert simpletest theme tables to table #type #1938926-45: Convert simpletest theme tables to table #type
This patch almost brings that back up to the speed before that conversion, but maybe you have some ideas on that beefy part of the system. (My only guess is the element_children() basically doubles if not worse to rebuild and sort the table in prep for the final loop in the twig template).
Comment #52
webchickOk! Effulgentsia kindly stepped me through this patch for ever an hour. This patch is great! Hugely cleans up a big mess in drupal_render(), and improves performance as a nice side-benefit. :) Any concerns I had (for example, the repeated copy/paste of certain things like page_top/page_bottom hunks) we have a follow-up for already: #2327277: [Meta] Page rendering meta discussion. I also wondered if this needs a change notice, since a lot of the patch is changing drupal_render() to drupal_render()/drupal_process_attached() but Alex pointed out that this should only affect a small handful of modules that are declaring Yet Another Page Render Endpoint, like Panels module. Fair enough.
Elsewise the patch is extremely well-documented and has some easy-to-read tests as well. Looks good to me.
Committed and pushed to 8.x. Thanks!
Comment #54
fabianx commentedCongratulations again, Wim!!!
Comment #55
wim leersHurray :)
Comment #56
fabianx commentedOpened child issue #2331393: drupal_render() recursive parameter usage is unclear, safe default should be drupal_render(x) as its related to usage of drupal_render() now.
Comment #57
catchWas away for the last 20 comments of this but looks like it went in a very nice direction!
Comment #58
wim leers@catch: very glad to see your approval :)