Problem/Motivation
Improve performance of menu tree rendering.
+
Correctly cache menu tree rendering.
- See profiling in #144 for indications what the impact of an improved menu tree rendering (one that leverages render caching) is. (Also see #97 for the current problematic performance.)
- In current D8 profiling, menu rendering consistently shows up as one of the biggest problems.
- Finally, #2158003: Remove Block Cache API in favor of blocks returning #cache with cache tags enabled render caching of menu blocks, but wrongly assumed that all menu links can be cached per role. Current HEAD is hence broken in that regard (steps to reproduce: create two non-admin users of the same role, give them the "view own unpublished content" permission, let create user A an unpublished node, put that in a menu that's in a menu block, look at that menu block with user B and you'll see the link to the unpublished node).
Proposed resolution
A number of proposed resolutions were discussed early on in the process, but then the issue became silent (probably due to the complexity).
Over the past few months year, lots of performance improvements and groundwork went in, that make this patch much more feasible. Notable changes include:
- #2287071: Add cacheability metadata to access checks — without this, we'd not be able to know which variations of a menu tree to render.
- #2479767: Fix MenuLinkInterface::isCacheable(): remove it in favor of implementing CacheableDependencyInterface — without this, we'd not be able to know which variations of a menu link to render.
- #2335661: Outbound path & route processors must specify cacheability metadata — without this, we'd not be able to know which variations of a URL to render.
- #2429257: Bubble cache contexts — without this, this solution would have to be much more complex; much more in line with what the pre-#144 patches did.
The changes to menu tree rendering
This is the stuff that this issue is all about.
DefaultMenuLinkTreeManipulators::checkAccess()
now sets anAccessResult
object (which implements bothAccessResultInterface
andCacheableDependencyInterface
). This ensures that the final menu tree contains all the necessary cacheability metadata during rendering.MenuLinkTree::build()
now tracks the cacheability metadata of both access results and the links it renders, and then applies that cacheability metadata to the the render array it returns. The cacheability of rendered URLs is taken care of inmenu.html.twig
, becauseMenuLinkTree::build()
does not actually render URLs, the Twig template does.MenuLinkTree::build()
just figures out *which* links to render.
(This means that any code that was transforming a menu link tree, and included an access-checking menu link tree manipulator, and manually rendered (i.e. not usingMenuLinkTree::build()
) the end result, must now perform access checking in that manual rendering.)SystemMenuBlock::getCacheContexts()
now no longer hardcodes certain cache contexts, which was done to enable caching of menu blocks, despite that being very much flawed (it just assumed that caching per role was sufficient, which it clearly is not).
Combined with the notable changes listed above, particularly the cache context bubbling, that yields a correct end result: the solution is actually very simple now that we have bubbling of cache contexts! It's just a matter of gathering the cacheability metadata of the things a menu tree render array depends upon, and setting it on the render array.
Note that any menu links that contain a CSRF token get max-age = 0
, which means that the entire menu will not be render cached in those cases. That's already tested by MenuLinkContentCacheabilityBubblingTest
and was already fixed in #2335661: Outbound path & route processors must specify cacheability metadata. This means that those menus containing such superdynamic links will not be cached, which is a fine trade-off.
Full unit test coverage in \Drupal\Tests\system\Unit\Menu\MenuLinkTreeTest
. See #170 for why this test coverage should be enough.
Other changes
- Changed
MenuParentFormSelectorInterface::getParentSelectOptions()
, so that that too is able to gather the necessary cacheability metadata. It's not strictly necessary for this issue, but it is within scope since it touches upon the same areas — we do want the forms using the menu parent form selector to be cacheable as well. Hence these changes. Disruption is approximately zero, since this is very rarely used. - Updated all code that either rendered menus in different shapes or showed a menu parent form selector to have the proper cacheability metadata, plus updated the test coverage.
- Added
Element::isEmpty()
— pure API addition, zero disruption. - Lots of test coverage updates/additions.
Follow-ups
Follow-ups that are blocked on this and will further improve cacheability/performance:
- #2254865: toolbar_pre_render() runs on every page and is responsible for ~15ms/17000 function calls
- #2483181: Make book navigation block cacheable
- #2483183: Make breadcrumb block cacheable
Remaining tasks
None.
User interface changes
None.
API changes
- API break:
MenuLinkTreeElement::access
must now be NULL or anAccessResultInterface
object, it used to be either NULL or a boolean. - API addition:
MenuParentFormSelectorInterface::getParentSelectOptions()
has gained a new optional parameter. - API addition:
Element::isEmpty()
Beta phase evaluation
Issue category | Task because lack of menu render caching is not a bug, but the poor performance of menu rendering is very important to solve. |
---|---|
Issue priority | Critical because this has a big performance & security impact. |
Prioritized changes | The main goal of this issue is performance |
Disruption | Minimally disruptive for core/contributed and custom modules, because it breaks BC for code that is directly transforming (manipulating and/or rendering menu trees. Read the CR for details. |
First attempt by Wim Leers
Problem/Motivation
Improve performance of menu tree rendering.
- See profiling in #97 for indications what the impact of an improved menu tree rendering (one that leverages render caching) can be, which simultaneously clearly shows the current problematic performance.
- In current D8 profiling, menu rendering consistently shows up as one of the biggest problems.
- Finally, #2158003: Remove Block Cache API in favor of blocks returning #cache with cache tags enabled render caching of menu blocks, but wrongly assumed that all menu links can be cached per role. Current HEAD is hence broken in that regard (steps to reproduce: create two non-admin users of the same role, give them the "view own unpublished content" permission, let create user A an unpublished node, put that in a menu that's in a menu block, look at that menu block with user B and you'll see the link to the unpublished node).
Proposed resolution
A number of proposed resolutions were discussed early on in the process, but then the issue became silent (probably due to the complexity).
Over the past few months, lots of performance improvements and groundwork went in, that make this patch much more feasible.
The current proposed approach is in #96, initial patch in #97. This approach was proposed by Wim Leers, with a crucial part being thought up together with catch. It's now actively being worked on towards a more reviewable whole, after initial approval from msonnabaum.
The high-level explanation of what this issue does can be explained by reading the docblocks for the refactored
MenuTreeInterface
and the newly addedCacheableAccessCheckMenuTreeManipulator
(see below).
The former had to be refactored to allow the "menu tree" abstraction to be used everywhere in Drupal core, including for selecting a parent menu item and the menu administration UI.
The latter contains all the logic to analyze the access cacheability of menu trees and to efficiently (optimally) apply uncacheable access checks.I've also added a new section to the Menu API documentation in
menu.inc
, for the very high-level explanation:* @section Rendering menus * Once you have created menus (that contain menu links), you want to render * them. Drupal provides a block (Drupal\system\Plugin\Block\SystemMenuBlock) to * do so. * * However, perhaps you have more advanced needs and you're not satisfied with * what the menu blocks offer you. If that's the case, you'll want to: * - Instantiate \Drupal\menu_link\MenuTreeParameters, and set its values to * match your needs. * - Potentially write a custom menu tree manipulator, see * \Drupal\menu_link\DefaultMenuTreeManipulators for examples. * - Call \Drupal\menu_link\MenuTree::build() with your menu tree parameters, * this will return a menu tree. * - Pass the menu tree to \Drupal\menu_link\MenuTree::render(), this will * return a renderable array.
Then, still at a high level, but with considerable more detail
/** * Defines an interface for building and rendering trees of menu links. * * The main goal of this service is to, given a menu name, build (::build()) the * corresponding tree of menu links. * Building the menu consists of two phases. First: loading the menu links in * the given menu. Second: transforming this list of menu links into a tree (by * looking at their tree metadata) and applying tree manipulators. Tree * manipulators can perform simple tasks like translating the title of all menu * links in a tree, or more complex tasks like access checking (which will * remove inaccessible menu links), or even very complex tasks like extracting a * subtree from the tree according depending on the active trail. * Which links are loaded and which tree manipulators are applied can be * specified in the MenuTreeParameters you pass to ::build(). * * If desired, that tree of menu links can then be rendered (::render()). * * * @section rendered_menu_tree_cacheability Rendered menu tree cacheability * * Unfortunately, because Drupal only wants to show menu links to a user that * are accessible for that user, we have a performance problem. A menu consists * of menu links arranged in a tree. Each menu link points to a route (or an * external URL). Each route can have any number of access checks defined. And, * finally, those access checks can be of arbitrary complexity, and therefore of * arbitrary cacheability. Some may be globally cacheable, others per role, yet * others per user, per page, per language, and whatnot. Some may not even be * cacheable at all. Caching per user, for example, is usually not an option: * the cache hit ratio would be too low on a typical Drupal site. * * Access checking is performed by menu tree manipulators. The simplest possible * manipulator will just apply access checks for the given user, and end up with * the correct result. Unfortunately, the resulting tree will only be cacheable * per user. To allow for more cacheable access checking, ::render() allows each * link to have a 'needs_dynamic_access_check' property. Those menu links will * be rendered into render cache placeholders. See ::render() for details. * * @see \Drupal\menu_link\DefaultMenuTreeManipulators * @see \Drupal\menu_link\CacheableAccessCheckMenuTreeManipulator */ interface MenuTreeInterface {}
/** * Provides a very advanced tree manipulator for optimal menu render caching. * * By default, this menu tree manipulator is configured to cache menu links * access (::setAccessCachingContexts()) per user role. * * We can classify access checks as non-dynamic (hence cacheable, per role by * default) or dynamic (needing to run on every page load, hence non-cacheable, * because not cacheable per role — again, by default). This service exploits * that property. * * We can then analyze the cacheability of the access checks in a given menu * tree (::analyzeAccessCacheability()), and if there aren't any menu links with * dynamic access checks, then the rendered menu is fully cacheable. * In the other case, we calculate and cache the dynamic access check results * per user (::getAccountDynamicAccess()). Those cached results are tagged with * the @link cache cache tags @endlink associated with those dynamic access * checks, to ensure that when the (cached) result might change, we run the * dynamic access checks again. * Now that we have deep insight in the cacheability of a given menu, the entire * menu tree can be render cached. * * Using this access checking menu tree manipulator instead of the simpler yet * uncacheable \Drupal\menu_link\DefaultMenuTreeManipulators::checkAccess(), * the entire menu tree can be rendered in a way that performs well * (\Drupal\menu_link\MenuTreeInterface::render()). Those menu links that have * >=1 dynamic access checks are rendered as render cache placeholders. * Then, on a page load with a warm cache, the rendered menu can be retrieved * from the render cache, the dynamic access check results for the current user * are retrieved from the cache once, and a #post_render_cache callback gets * applied (\Drupal\menu_link\MenuTreeInterface::renderItemPlaceholder()) to * each placeholder. That callback then has all the necessary information to * decide whether to render, and to do the rendering itself, so it can occur * very efficiently. */ class CacheableAccessCheckMenuTreeManipulator {}
Remaining tasks
Land blocker: #2256521: [META] New plan, Phase 2: Implement menu links as plugins, including static admin links and views, and custom links with menu_link_content entity, all managed via menu_ui module (must-have)- Land blocker: #2287071: Add cacheability metadata to access checks (must-have)
- Land blocker: #2273277: Figure out a solution for the problematic interaction between the render system and the theme system when using #pre_render (must-have)
- Land blocker: #474004: Add options to system menu block so primary and secondary menus can be blocks rather than variables (should-have)
- Land blocker: #1869476: Convert global menus (primary links, secondary links) into blocks (should-have)
- Land blocker, either of: #2335661: Outbound path & route processors must specify cacheability metadata or #2351015: URL generation does not bubble cache contexts
- Review, profile
- Get to RTBC
- Commit
User interface changes
TBD
API changes
- Access check cacheability
- Addition:
AccessManager::getCacheableAccessChecks()
- Addition:
\Core\Cache\CacheabilityAffectorInterface
- Change:
\Drupal\Core\Routing\Access\AccessInterface
now also implements the newly addedCacheabilityAffectorInterface
- Addition:
\Drupal\Core\Routing\Access\AccessBase
- The above allows us to mark us the following access checks:
- cacheable per role:
FormModeAccessCheck
,ViewModeAccessCheck
,LoginStatusCheck
,PermissionAccessCheck
,</code>, <code>RegisterAccessCheck
,RoleAccessCheck
,ViewsAccessCheck
- cacheable per user:
EntityCreateAccessCheck
,NodeAddAccessCheck
,ShortcutSetSwitchAccessCheck
,ViewOwnTrackerAccessCheck
- globally cacheable:
DefaultAccessCheck
,ThemeAccessCheck
,DefinedTestAccessCheck
,
TestAccessCheck
- Menu tree rendering
MenuTree(Interface)
is now much more focused: it's only purposes are to build a menu tree and render it.- For a large part, this is thanks to splitting the convoluted-and-impossible-to-grok logic in
MenuTree::buildAllData()
,MenuTree::buildPageData()
andMenuTree::buildTreeData()
into smaller pieces that are reusable, rather than the same logic being repeated in multiple places with subtle differences. The logic responsible for determining the query to retrieve a menu's menu links according to certain parameters now lives in a newMenuTreeParameters
object- The access checking, translating, active trail-expanding etc. logic no longer lives partially in
menu.inc
and partially inMenuTree
(partially in those); it now all lives inMenuDefaultTreeManipulators
- The active trail handling is unchanged, but now lives in
MenuActiveTrail(Interface)
- A
MenuTreeItem
value object has been added, to make the resulting menu tree (ofMenuTreeInterface::build()
) more well-defined/formalized and hence more reliable/less brittle. A menu tree now consists of menu tree items, just like before, but instead of it being a particularly structured array, it's now a tree ofMenuTreeItem
s. Menu tree manipulators receive and manipulate these.- … more coming. Stay tuned.
Related Issues
#1137920: Fix toolbar on small screen sizes and redesign toolbar for desktop
#1814932: Caching strategy for D8 toolbar
#1927174: Complete the work to make the Toolbar administration menu sub tree rendering more efficient
Original report by effulgentsia
#1137920: Fix toolbar on small screen sizes and redesign toolbar for desktop is a critical feature: otherwise, D8 ships with a terrible mobile experience. Therefore, making this issue a critical task.
One of the aspects of the current design in that issue is to from any page, be able to drill down to any admin link with purely client-side logic. In this way, it shares a resemblance to Admin menu.
Since rendering (whether all the way to HTML, or to a JSON tree containing access filtered and argument resolved URLs) an entire menu tree is expensive, this requires caching. Admin menu 3.x implements its own caching with fully rendered HTML cached in {cache_menu} with cid of 'admin_menu:' . $user->uid . ':' . session_id() . ':' . $language->language;
. In other words, a per-user cache scoped to admin_menu.
In comment 110 of that issue, catch requested that we not port that approach to a toolbar-specific cache, but rather solve the problem generally in the menu system.
I see this issue as having three parts (perhaps not all of them need to be solved as part of this issue):
- Make the output of menu_build_tree() not page-specific. Because the output is already user-specific (access filtered), and a cache that is both per-user and per-page is close to useless. However, it can only be non-page-specific when called with empty 'expanded', 'active_trail', and 'only_in_active_trail' build parameters. I think it's fine if we only cache the output when those conditions are met, and assume that consumers of this cache are therefore use cases that can implement their own tree collapsing client-side. Any thoughts on this or whether there's any other subtle problems with reusing this information across different pages?
- Instead of caching menu_build_tree(), we'd get more performance gains by caching a client-consumable rendered form of it, either the result of menu_tree_output() (fully rendered HTML with ULs and LIs), or a JSON tree (possibly with anchor tags rendered but not the UL/LI wrappers). If we only render a JSON tree with anchor tags, then we just need to make sure the 'active' class isn't added, which isn't hard. But then the JS code that adds ULs and LIs potentially needs to be made consistent with whatever theme overrides of theme_menu_tree() and theme_menu_link() are in use by the active theme. OTOH, if we render the full HTML, then we need to make menu_tree_output() not page-specific, both for its active trail handling, and by documenting that preprocess and theme overrides of 'menu_tree' and 'menu_link' shouldn't have page-specific logic.
- If this is to be truly reusable outside of toolbar, it would be helpful to have some other use case to work with in designing it. Catch suggested core's menu block. However, this would imply converting that to a JS-based solution (for rendering what's expanded vs. what's collapsed based on the current page), and we'd have to figure what the no-js behavior should be. This could actually be a nice performance gain for Drupal as long as we can come up with a no-js fallback that's acceptable.
Any thoughts on the above, or something I missed?
Comments
Comment #1
moshe weitzman CreditAttribution: moshe weitzman commented1. I think we have to accept #1 and make this a non page-specific cache. I agree that the caller can tweak from there. I'd be fine with tweaking happenning in PHP or JS.
3. The menu block is only relevant if we don't show the mobile toolbar to desktop sized browsers. Why would one want a the menu block along with a toolbar? The only other use case I can think of is quick-navigation widget like what some of the toolbar designs have shown. It would be an autocomplete which whisks you to the selected admin page upon submit.
Comment #2
catch@moshe, with the menu block I meant it'd be good if we could cache that more efficiently, currently that's some of the least efficient caching we have (has to be calculated, if not stored uniquely, per page, access checks (which can load entire views, entities etc.) run during runtime, full rendering from the big array that we store in the cache). It also happens to be in core already.
Not sure why we'd need to cache by both user ID and session ID, that potentially means different cache entries for anonymous users if they have a session and I can't think of a use case for that - maybe admin_menu has some per-session logic it specifically has to account for or something?
Comment #3
Dries CreditAttribution: Dries commentedTagging it with Spark, as it is relavant for Spark.
Comment #4
jessebeach CreditAttribution: jessebeach commentedre: Personally, I'd like to see all of the menu state code (active, expanded, active-trail) move from the server to the client. It gives us the boost of eliminating per-page cacheing.
I think we have some room to explore different combinations of server-side versus client-side DOM list building. Our options are:
I just ran some base-line speed tests with a small and obscenely-large set of links in unordered lists. I used Charles to simulate throttled bandwidth and measured performance with Chrome's Network tab. Here are the results for combined wait/receive time:
110 links in a nested (2 levels)
ul
element.unthrottled/cable: 3ms
256 kbps/DSL: 303ms
3g: 696ms
1110 links in a nested (3 levels)
ul
element.unthrottled/cable: 4ms
256 kbps/DSL: 2670ms
3g: 1330ms
5555 links in a nested (3 levels)
ul
element.unthrottled/cable: 4ms
256 kbps/DSL: 14048ms
3g: 4560ms
11,110 links in a nested (4 levels)
ul
element.unthrottled/cable: 6ms
256 kbps/DSL: 27900ms
3g: 84300ms
A standard installation of D7 with the Administration module installed (so that we get dropdowns) has 154 links in the toolbar menu.
The Drupal Gardens site, which has the Administration menu installed and a lot of modules enabled has 683 menu links loaded on each page. That is still well shy of the 1110 links in the chart above where page load begins to be affected on 3g by a large menu. Still, it's enough time to warrant exploring hybrid options to load top levels of a menu and deeper sub-levels with some asynchronous method.
To sum up, we shouldn't have to treat the toolbar menu with special gloves in terms of cacheing. We put in sensible, per user cacheing (perhaps even per-role? I'm waiving my hands here, this is not my area of expertise), returned a rendered HTML list and leave the state decoration (current page, active path) for the client to deal with (that I can do). The Administration menu module already has a lot of excellent front end code we can leverage for that part of the built-out.
We could also get rid of this option for menu links, which has always driven me mad, especially when I need to edit every link in a complex menu to get it to display as a full tree.
Comment #5
effulgentsia CreditAttribution: effulgentsia commentedJust wanted to post a work in progress patch. I'm working with a devel generated menu block with 300 links upto a depth of 4, caching the result of menu_build_tree() per-user, but not per-page. I'm caching subtrees, since retrieving a fully expanded tree from cache is both memory intensive and slow.
I'm not seeing any performance benefit from it yet, but that's because most of the savings can come from caching the result of l(), and I'm not doing that yet, but will work on that next.
Comment #6
effulgentsia CreditAttribution: effulgentsia commentedOk, this once caches the l().
Comment #8
effulgentsia CreditAttribution: effulgentsia commented#6: menu-cache.patch queued for re-testing.
Comment #10
effulgentsia CreditAttribution: effulgentsia commentedComment #11
effulgentsia CreditAttribution: effulgentsia commentedHere's a benchmarking script that can be applied on top of #1137920-114: Fix toolbar on small screen sizes and redesign toolbar for desktop. It requires devel module enabled.
Comment #12
effulgentsia CreditAttribution: effulgentsia commented#10 includes a hunk that outputs some times above menu blocks of devel generated menus. On a 300 link menu where all links are node links and I have no node access module in use, when viewing a node page that returns 25 links to the browser (only menu items that are not children of collapsed parents), I get:
#11 can be applied to the toolbar patch in that other issue. That benchmarks the time to get, build, and render a fully expanded Management menu. For that, I get:
These numbers are relative to an approximately 100ms total page response, so correspond roughly to percentages.
Comment #14
sunI'm not comfortable with caching access/per-user. It will result in too many cache items on large sites (essentially: users x menus).
But the idea of caching the "rendered" link makes sense to me.
However, that requires a lot of additional context info for the cache item that contains the rendered links:
- HTTP vs. HTTPS
- conf_path() / HTTP_HOST / domain (in case an absolute link is contained)
- language
- last timestamp of a changed string tanslation
- theme key (also: theme vs. admin_theme)
[potentially more; these are the most obvious only]
- ?destination
- active trail
- session (link tokens)
Furthermore, the link rendering would have to happen in a special context that is exempt of user permissions; i.e., all links need to be accessible to be rendered and cached.
Inaccessible links would be removed when the cached menu link tree + cached menu links are rendered for the current user. This could potentially allow us to merge the rendered links into the cached menu link tree even.
[snip]
Now that I wrote this, I just added ?destination and active trail to above list of cache contexts. These are the two major problems that admin_menu solves after the fact on the client-side:
admin_menu makes sure that the cached output does not contain an active trail, and dynamically injects the active trail classes into the rendered menu links for the current page.
admin_menu also replaces every instance of
?destination=...
in all links that have a destination for the currently rendered page.Lastly, admin_menu caches per user, because links may contain drupal_get_token() session token query parameters.
Doing that makes sense for admin_menu, because there is usually only a very small subset of administrative users on a site, even on large-scale sites. E.g., even if there are 1,000,000 users on a community site, there are usually only ~10-500 users with access to the site administration, which results in the equivalent amount of caches.
Hrm. Overall, I do not think it is feasible to change this for all menus in core. The performance of rendering large menu trees is certainly a problem, but from my perspective, the current build/access/render process is well thought-out and takes all context possibilities into account that might have an impact on the final output.
admin_menu is only able to choose a different approach, because it is a single use-case, because it knows exactly how and where the rendered menu tree is used, and because it renders the specific menu tree of administration links. Without that advantage, the amount of contexts is factually unlimited.
Comment #15
effulgentsia CreditAttribution: effulgentsia commentedThanks for the review, sun. Ok, how about this instead?
I removed the calling of l() from _menu_link_translate() for all the reasons given in #14. I think this is worth looking into more, but perhaps in a different issue. Also, we shouldn't really be caching l(), just url(), but all the same issues apply to that as well.
I also changed menu_build_tree() to only cache localized menus that aren't dealing with active trail, per the issue summary. This means this caching is no longer being automatically applied to regular menu blocks. Applying it to menu blocks would need to be done in a separate issue, together with the javascript for applying the active(-trail) classes client-side.
Note that since even the existing toolbar builds a menu tree (with depth=1) with no active trail handling, this patch does implement per-user caching of that. That might not actually be a good thing, since localizing a 1 deep toolbar takes very little time, but meanwhile, we're adding a cache record per user (who has access to the toolbar). But, since catch asked to explore this issue generically, that's the current patch behavior.
I also changed theme_menu_link() to not call l(), but render the anchor tag directly. First of all, at DrupalCon Munich, when discussing template conversions to twig, the themers present all asked for putting anchor tags in menu(-link).twig instead of calling l() on TX grounds alone. Secondly, decoupling from l() lets us override 'active' class handling, and pre-generating/caching url().
Finally, this patch adds a demo hunk to toolbar.module to (in the presence of a 'toolbar-tmp-depth' query string) append an expanded Management menu of the desired depth along with the number of ms it took to build the tree and render it. It is here that I also added render caching for it. Given #14, I don't think we want to add render caching into menu_tree_output() itself (and therefore, this patch doesn't), but doing so in the use-case specific code is easy as this patch demonstrates.
By employing both menu_build_tree() caching and render caching, this I think solves all server-side performance issues of #1137920: Fix toolbar on small screen sizes and redesign toolbar for desktop. We still might want to implement client-side localStorage caching in that issue though for mobile bandwidth optimization. However, sun is calling into question the premise of per-user caching on sites with many users, so additional feedback from people about that would be helpful.
Comment #16
sunHonestly, I think this is a lot of complexity that menu.inc should not contain.
It wasn't really easy to follow/understand what the new code is doing, and under which (special) conditions (and when not).
Is there any way we could allow modules/consumers to achieve the same, without adding even more complexity to menu.inc?
Although I'm still not sure whether it is appropriate to "hack" a core subsystem in this way, mainly to improve the performance of a number of core/contrib modules that I can probably count on one hand?
That said, I also wonder what the actual (performance) situation with the new Symfony router looks like - the new router itself shouldn't have an impact, but our current menu link tree building is deeply intertwined with the (old) router...
Comment #17
sunReclassifying as a feature, since the only issue being blocked by this is a feature request as well, and I assume we surely don't want to block other, ready patches due to this issue.
#15 still caches per-user (and lacks session_id(), btw). An acceptable amount of caches for such a generic implementation would be per-role[s], but that would inherently break menu access checks/restrictions for link-specific endpoints being backed by e.g. node_access().
Comment #18
catchI'm confused by sun's per-user caching comment.
Current menu caching:
- menu trees are calculated on every page, then the hash of the tree is calculated.
- two cache entries are stored, a cid for the page, pointing to the hash. Then a cid with the hash, pointing to the tree.
This is designed to prevent massive filling-up of the menu cache bin (which was a critical Drupal 6 bug report).
If we move to per-user caching, then that doesn't necessarily mean more cache entries at all.
On most sites I work with, there's a vast quantity of traffic to tens or hundreds of thousands of unique paths every day from search engine crawlers. These currently require a cache entry for each individual page per menu. Per-user caching would mean a single entry per-menu for all of those bots, since they'd all be anonymous requests.
There'll be some sites with many, many authenticated users, but you'd need tens to hundreds of thousands of unique authenticated user requests each day to get anywhere close to the number of cache misses that there currently are with core.
Once we have a generator (the Symfony version of url() for internal paths), I'd expect menu tree generation to get more expensive, additionally if menu items become entities that'll probably come with some performance overhead as well. See also the discussion on #1793520: Add access control mechanism for new router system which is sort of the counterpart to the toolbar issue since they'll compound each other quite significantly.
Comment #19
catchGiven #916388: Convert menu links into entities I think this is a more appropriate status. It may need to be bumped to critical again if either of those patches goes in before this one and we find a measurable performance regression.
Comment #20
effulgentsia CreditAttribution: effulgentsia commentedPer #18, I think this issue continues to be appropriate, even as a major task, for reasons other than Toolbar. For Toolbar though, I think #1814932: Caching strategy for D8 toolbar is better.
Comment #21
sunI doubt that we have any sophisticated tests for menu caching and cached rendering currently. So if we want to change this, then I'd suggest to start with writing tests for all the special cases outlined in #14.
Comment #22
corbacho CreditAttribution: corbacho commentedI would like to support Jesse's and eff idea of a JSON rendered output
The output can be embedded directly in the HTML (printed in the template), to avoid the delay. There is no need to make it via AJAX, but it could be used the same code to create an on-demand expanding tree. #1814932: Caching strategy for D8 toolbar
This solution will help to make a fast search/autocomplete, since the data is already in native format. No need to parse the html.
I did it for a prototype, and it works (except that tasks are mixed with regular links). For a simple example of how to use the rendered json data in JavaScript to render a navigation menu: (see
var jsontree
)http://drupalmotion.com/sites/default/files/demos/toolbar/demo1.js
The big caveat (the elephant in the room): This won't work in Javascript disabled browsers. So it's not a generic solution for every single menu. But it will adapt very well to the toolbar case (autocomplete, dynamic collapsing tree, etc )
Comment #23
YesCT CreditAttribution: YesCT commentedThis could probably use a issue summary update (doc: http://drupal.org/node/1427826)
since this is at needs review, instead of needs work... I'm guessing a reroll (http://drupal.org/patch/reroll) might be useful.
Comment #24
YesCT CreditAttribution: YesCT commented#15: menu-cache.patch queued for re-testing.
Comment #26
jibranre-roll
Comment #28
YesCT CreditAttribution: YesCT commented#26: 1805054-26.patch queued for re-testing.
Comment #30
YesCT CreditAttribution: YesCT commentedI'm not sure what "failed to run tests" indicates.
the testbot faq: http://drupal.org/project/testbot might help
@jibran, where there any conflicts when you did the re-roll?
Comment #31
YesCT CreditAttribution: YesCT commentedMaybe check the testbot number...
this might also provide a clue: #1878216: Bot #664 disabled (repeatedly failed to run tests)
Comment #32
jibrannope no conflicts simple merges
here is the merge if it helps.
Comment #33
jibran#26: 1805054-26.patch queued for re-testing.
Comment #35
jibranReplaced a call to
drupal_array_set_nested_value()
withNestedArray::setValue()
as per#1705920: Convert all calls to procedural drupal_array_*() functions to Drupal\Component\Utility\NestedArray
Comment #36
jibranAnother reroll had conflict in menu.inc
Resolved like this
Next reroll will be in next 2 months.
Comment #37
xjmComment #38
nod_Raised related #1927174: Complete the work to make the Toolbar administration menu sub tree rendering more efficient to critical.
Comment #39
benjifisher#36: 1805054-36.patch queued for re-testing.
Comment #40
bsnodgrass CreditAttribution: bsnodgrass commentedI will work on the issue summary
Comment #41
bsnodgrass CreditAttribution: bsnodgrass commented#36: 1805054-36.patch queued for re-testing.
Comment #43
bsnodgrass CreditAttribution: bsnodgrass commented#36: 1805054-36.patch queued for re-testing.
Comment #43.0
bsnodgrass CreditAttribution: bsnodgrass commentedupdated issue summary
Comment #43.1
bsnodgrass CreditAttribution: bsnodgrass commentedUpdated issue summary. Test bot was misbehaving at the time of the retest.
Comment #43.2
bsnodgrass CreditAttribution: bsnodgrass commentedUpdated issue summary.
Comment #43.3
bsnodgrass CreditAttribution: bsnodgrass commentedUpdated issue summary.
Comment #43.4
bsnodgrass CreditAttribution: bsnodgrass commentedUpdated issue summary.
Comment #43.5
bsnodgrass CreditAttribution: bsnodgrass commentedUpdated issue summary.
Comment #45
Wim LeersRelated: #2005644-35: Use client-side cache tags & caching to eliminate 1 HTTP requests/page for in-place editing metadata, introduce drupalSettings.user.permissionsHash, especially the "More generally…" part.
Comment #46
Gábor HojtsyRerolled and fixed the constant notice issue.
Comment #48
Gábor HojtsyEliminate use of global $user and document new arguments (and add type hints). Reviews welcome!
Comment #50
Gábor HojtsyOf course languages also changed to have 'id' properties not 'langcode' in the meantime :)
Comment #52
Gábor HojtsyI don't know where did I get 'account' from as the attribute name. It should be '_account'. Also uid is not a public property, so we need to use id(). Should be better.
Comment #54
jessebeach CreditAttribution: jessebeach commentedI ran into a problem with the output of the
cache_menu
, as well. I had to change the bin name fromcache_menu
tomenu
.Calling
language()
has apparently been deprecated as well.Comment #55
Gábor HojtsyLet's go testbot!
Comment #56
jessebeach CreditAttribution: jessebeach commentedSame patch as #54, this time without the
do-not-test
, so that testbot takes notice.Comment #58
jessebeach CreditAttribution: jessebeach commentedeffulgentsia, Gábor Hojtsy and I had a chat about how to further improve the cacheability of the menus. We came up with two changes that reduce the contextual variability of a rendered menu, shifting context and behavior decisions from the server to the client.
1. Deliver the full menu tree to a specified depth; qualify expanded branches with a data attribute; hide unexpanded, non-active-trail branches with client-side code.
Whether or not a menu is expanded is a decision that should be made on the client side, where context is available. The suggestion to expand a menu can be made on the client side, as is done today, but instead of shifting the content that gets returned based on the context of the request path, we simply deliver the same menu structure, with an attribute like
data-drupal-menu-expand
on theli
elements that should be opened, and let client-side code do the proper presentation and behavior embellishments.2. Render the active trail qualification on the client-side using the currentPath setting.
Active trail in a menu is easily derived on the client side. We shouldn't have to make menus uncacheable just to inject this highly contextual bit of information.
Implementation
These changes will require some tweaking to the PHP code for menus, but not much. I will build the menu client side code to do the expansion and active trail marking. Much of this code exists in the Toolbar module, so I plan to pull that code into more generic files and make the behaviors available to any module.
Comment #59
Gábor HojtsyIs data-drupal-menu-expand a kind of data that the toolbar would need or does it keep track of what was opened on its own anyway? Eg. if I navigate to http://myd8site/#overlay=admin/config/content/formats now via the menu, so long as I reload the page, the menu will not be open. The configuration item will be marked as active (based on it being generated from the menu system with active trail I guess) but none of the rest of the items are open. Is this the expected behaviour? Then I don't see a need for additional context specific data in the menu tree. (Not having that would let us cache the menu client side for multiple pages as well, which would be a huge win). If we don't need that info, then this issue is getting pretty close I think?
Comment #60
jessebeach CreditAttribution: jessebeach commentedThe
data-drupal-menu-expand
would indicate the state of the expand checkbox on the menu link config. Presently, if the checkbox is unchecked, we don't deliver the HTML for the sub tree. Under this the proposed behavior, we deliver the full tree for all links (to the depth requested) and those links that have the expand checkbox checked will have thedata-drupal-menu-expand
attribute applied to them.Comment #61
Gábor HojtsyOh, great. The menu code does expansion based on the expand flag and the active trail, so I mashed up the two too much. Thanks for the clarification! That sounds good. Looks like an easy addition to this patch.
Comment #62
jessebeach CreditAttribution: jessebeach commentednod_ has been working on active link decoration in the client (removing the 'active' class from the l() function) here: #1979468: ".active" from linkGenerator(), l() and theme_links() forces an upper limit of per-page caching for all content containing links.
Comment #63
Gábor HojtsyAfter an epic debug session I figured out the patch fails because on 403 pages (in general!), the request does not contain an _account at the point when the menu system wants to get it (for toolbar_toolbar()). I reproduced this with other 403 pages like the filter format delete page. Only the shortcut set test catches this because the toolbar tests have no 403 testing and only the shortcut tests have toolbar enabled as well.
This is pretty late in the request in toolbar_toolbar() (my original assumption was that it is related to routing items being resolved, but that was a dead end), so why that would be is not entirely clear to me. However, if we don't have a user, we cannot really cache it for the user anyway (and we should not cache it for anonymous), so I added one more condition and no caching when there was no user identified. This works on 403 pages for me now.
Did not yet add the data attribute, but that would be next.
Comment #64
catch@Gabor sounds like you ran into #2062151: Create a current user service to ensure that current account is always available.
Comment #65
jessebeach CreditAttribution: jessebeach commentedAnd #2064585: Shortcuts missing if you view a route's page, that doesn't have a corresponding hook_menu().
Comment #66
Wim LeersMore 403 problems if that's your thing: #2063303: A Drupal 8 AjaxResponse that must return a 403, returns a 403 and prints "A fatal error occurred: ".
Comment #67
Gábor HojtsySo we can work around #2062151: Create a current user service to ensure that current account is always available pretty simply here for now. Any concerns with this patch specifically? :)
Comment #68
jessebeach CreditAttribution: jessebeach commentedSince we're optimizing the generic menu building code, it might be possible to render a menu for a anonymous user, no? So what about cacheing menu output for the anonymous user? A list of navigation links exposed to non-authenticated users seems a likely case where we'd want to cache the rendering for anon users.
Comment #69
Gábor HojtsyNonono, we can and should (and already do) cache for anonymous. If the user is not there, we cannot tell if it was an anonymous user or not, so my comment just concerned that situation. If the user is there, the uid may still be 0 (drupal_is_anonymous() used to be useful for this, now there may be a method on the user). So long as we don't have a user, we cannot cache it by user :)
Comment #70
jessebeach CreditAttribution: jessebeach commentedI polled a few themers/site builders in my office to get some diversity of opinion on this topic. Here's what I came away with:
It's very frustrating to create a sub-menu item, go out to your site to preview the menu and find that the menu item isn't displayed on the page. This happens when the user fails to click the expand checkbox on the menu item's parent.
Solution: The expand checkbox should be selected by default.
We only want to hide sub-menus when JavaScript is functioning. In the case that a JavaScript error occurs on the page or JavaScript is disabled, we want sub-menus to be accessible, so a blanket display none of non-expanded sub-menus is not desired. For instance, the following code would be bad
Because it assumes that sub-menus are hidden until exposed. We rather want sub-menus to be accessible/visible by default and condensed/hidden as the context demands (progressive enhancement). We'll get this by adding a data attribute to sub-menu parent items that do not have their expand check box setting checked.
Solution: add a data attribute
data-drupal-menu-collapse
to sub-menu item parents that are not configured to be expanded.Comment #71
jessebeach CreditAttribution: jessebeach commentedThis seems to be the place in
menu_tree_output
where we want to intervene to get the data attributedata-drupal-menu-collapse
The problem I'm having is that
$data['below']
is not populated if the menu item has sub-items in general; it's only populated if the current request for data has resulted in sub-items being rendered. So maybe we need to intervene before getting to this function. If we always render the entire tree, then$data['below']
should be true when the menu item is a parent of sub-items.Comment #72
Gábor HojtsyI think this is out of scope for this issue. We deal with performance improvements here. Arguing about form defaults may push this off too far :/
Sounds like you suggest either having an expand or a collapse data attribute then (and none if the item is a leaf?). Can you clarify that?
Comment #73
jessebeach CreditAttribution: jessebeach commentedI'm spinning on the PHP part of this patch, so I'm going to jam in whatever I need to get the approximate output of the attribute I want on menu links with sub-items. I'll get the JS bits into the patch and hopefully someone else will correct the PHP part :)
Comment #74
Gábor HojtsyI see you may have hit some issues (also based on your recent tweets). Here is a simple version I worked out. This will put on a collapse or an expand attribute based on the setting for the menu item.
Note that the caching on the menu is pretty aggressive, so editing the menu item will not make it appear properly changed in the rendered tree. You need to clear cache for now.
Is this what you have been looking for? I double-checked that this makes collapse appear on items that did not have the checkbox checked and expand only on those that did.
Also fixed the performance proof code to do something useful :D On my machine the cached version is over 2x as fast. 11.6ms vs. 4.3ms.
Comment #75
Gábor HojtsyMaybe > 0 bytes would be better :D
Comment #77
jessebeach CreditAttribution: jessebeach commentedI'm working in #1979468-101: ".active" from linkGenerator(), l() and theme_links() forces an upper limit of per-page caching for all content containing links to get the active link and trail marking resolved.
Comment #78
Gábor Hojtsy#75: 1805054-74.patch queued for re-testing.
Comment #79
pwolanin CreditAttribution: pwolanin commentedThis seems really premature given how much change is left to do for menu links to make them work properly with routes.
Comment #80
Gábor Hojtsy@pwolanin: Wait, what this patch does it allows to skip active trail marking, so the rendered menu can be cached. How is that related to where the menu data comes from (routes or whatnot)?
Comment #81
pwolanin CreditAttribution: pwolanin commented@Gabor - ok, maybe it doesn't' need to be postponed, but a bunch of it will need to be re-written since e.g. url() will not be the way most links are rendered.
Comment #82
Gábor HojtsyIs there an expected timeframe on those conversions? We'd love to have this in sooner than later. :)
Comment #83
johnvDear core/ D8 menu maintainers, apologies for this cross-post.
Please take a look at the #1978176: Build menu_tree without loading so many objects, which contains a D7/D8 performance patch for the menu build (see the 'related issues' in the issue summary for the symptoms). (One error is a problem with cached menus..)
My main question is:
- is the proposed patch/solution also valid for D8? Since it does not touch the new menu router system?
- Is the current D7-menu building system still supported in D8, will it be fased out, or will it exist next to the new system?
Depending on the answer, I will continue to work on that issue.
Comment #83.0
johnvUpdated issue summary.
Comment #84
catchBumping to critical again. We can stick it back to major if profiling shows there's no problem, but that seems extremely unlikely.
Comment #85
martin107 CreditAttribution: martin107 commentedPatch no longer applies.
Comment #86
moshe weitzman CreditAttribution: moshe weitzman commentedCapturing a brainstorm on this with @effulgentsia and @msonnabaum. We noted that discussion on this issue predates #post_render_cache. So,
We also discussed that we will hopefully soon have #597236: Add entity caching to core. This affects the discussion here but it isn't quite clear how.
Comment #87
effulgentsia CreditAttribution: effulgentsia commentedThe key to #86.2.2 would be the assumption that menu link access checks could vary per user but not per page, while #86.1 can vary per page but not per user.
Comment #88
moshe weitzman CreditAttribution: moshe weitzman commentedWim just educated me about menu block caching since #2158003: Remove Block Cache API in favor of blocks returning #cache with cache tags. We now cache menu blocks per language and per role. Sites that do more crazy access checks can disable caching for this block and implement other strategy. Should we call this issue Won't Fix?
Comment #89
moshe weitzman CreditAttribution: moshe weitzman commentedOK, lets go with Won't Fix as per #88. Feel free to reopen if you disagree.
Comment #90
effulgentsia CreditAttribution: effulgentsia commentedI still think #86 is a good idea to implement in core, not punt to contrib. Downgrading to major though if there's no per-user access checks in core that justify this being critical.
Comment #91
effulgentsia CreditAttribution: effulgentsia commentedActually, wondering if "view own unpublished content" permission makes this critical. I.e., does menu block in HEAD cache a menu link to an unpublished node if the menu is first viewed by the node's author, and then display that link to other users of the same role?
Comment #92
pwolanin CreditAttribution: pwolanin commented@effulgentsia - yes, I think the bug you outline could happen. In Drupal 7 unpublished nodes were never shown in menu blocks. In Drupal 8 they are.
However, this seems like this could happen for any entity where you can have per-user access be different based on status or some other flag.
Comment #93
catchYes 'view own unpublished content' makes this critical.
@pwolanin we have #2099137: Entity/field access and node grants not taken into account with core cache contexts for contrib modules to affect render cache granularity, and #post_render_cache, so contrib has at least two methods to ensure cache correctness. However where the cache granularity is broken in core we should fix that in core.
Comment #94
Wim LeersI worked on #2254865: toolbar_pre_render() runs on every page and is responsible for ~15ms/17000 function calls yesterday, which showed huge perf win potential. 15% speed win on a full node page. 33 DB queries less. Turns out it was because I had stupidly failed to notice I was caching a menu tree… incorrectly, of course. Which brings me to this issue. I'm taking this on because it's sorely needed.
By making menu blocks cacheable, I also introduced that "view own unpublished content" bug.
I will be able to leverage some of #2224861: Cache SystemMenuBlock and BookNavigationBlock per active trail (currently cached per URL, this breaks on very long URLs). I'll first read through the entire issue, but will probably apply a strategy like the on in #86.
Comment #95
Wim LeersI've now been working on this quasi non-stop since #94 — a week ago. I'll post 2 comments: one with the general approach, and one with the patch that I have so far — but it will not apply to HEAD since I haven't found the time yet to rebase. You should be able to review the overall approach though.
Comment #96
Wim LeersThe 90% of the problem space that already has solutions
Having read this entire issue, I think sun's comment in #14 is the single most important comment:
Context, context, context. And in fact: dependencies, dependencies, dependencies. That's what this issue is all about and what makes it so complex: our menu system is so incredibly flexible, because it allows so many contexts/dependencies to affect the rendered result. At the same time, that's precisely why it performs so poorly!
Let's number all of these contexts, plus missing ones (emphasized):
conf_path()
/HTTP host/domain)The <code>.active
class. (Page-specific.)?destination
.Now, I think a few patches from the past few months greatly help with addressing this (same order as the above numbered list):
LanguageCacheContext
,ThemeCacheContext
,UrlCacheContext
,UserCacheContext
andUserRolesCacheContext
. By adding aDomainCacheContext
, we can easily cache per protocol/site in a multisite combination. Note that this also allows for site-specific contexts, such asCountryCacheContext
,CurrencyCacheContext
orWeatherCacheContext
.LanguageCacheContext
.ThemeCacheContext
.locale
cache tag is deleted, which will cause all cache items tagged with it to be invalidated..active
class problem was addressed in #1979468: ".active" from linkGenerator(), l() and theme_links() forces an upper limit of per-page caching for all content containing links, by introducingdata-
attributes on links that should get that class when applicable plus a#post_render_cache
callback that is applied to the entire page.MenuTreeInterface::getActiveTrailIds()
, which allows us to generate an active trail cache key, and hence cache per-active trail instead of per-page (which is much better because many pages have the same active trail). An even better solution might include?destination
problem was addressed in #2136507: Use client-side cache tags & caching to eliminate 1 HTTP requests/page for rendering Contextual Links for contextual links, but we can apply it here also (usesdrupalSettings.path.currentPath
plus a tiny amount of JS).Combined, this allows us to (again matching the above numbered list):
absolute => TRUE
, because the number of domains is always going to be limited..active
class problem; we can consider that a now-irrelevant cache context.?destination
problem; we can turn that into an irrelevant cache context.It should also be possible to alter a menu tree's rendering to depend on more cache contexts, because that way, you can retain the same cacheability, but allow for e.g. menu links that have access dependent on one of those cache contexts ("this link can only be accessed by people in Belgium").
The 10% of the problem space (that will be 90% of the work) that is not yet solved
This is, once again, the "arbitrary access checks" problem. i.e.
hook_entity_access()
/hook_node_access()
.The solution that catch and I came up with together:
_permission
requirement (or more generally, whose access checks are cacheable per role).#post_render_cache
callback. This will allow us to render cache the entire menu tree per role/language/theme/active trail/…, but still render those links that need dynamic access checks.#post_render_cache
can run as efficiently as possible, we calculate and cache the results of the dynamic access checks in a menu per user, and associate the cache tags of the route parameters that turn out to be entities after route parameter enhancing (all entities have cache tags as of #2217749: Entity base class should provide standardized cache tags and built-in invalidation!), so that whenever any of those entities change, the dynamic access checks results that are cached per user & menu are recalculated (e.g. a menu link to an unpublished node, and then suddenly that node gets published — cache tags ensure the correct invalidation happens).Comment #97
Wim LeersHelper at #2268971: Helper for #1805054.
Child issues created while working on this so far:
A few of these have landed, which allow this patch to become simpler.
Also filed #2273277: Figure out a solution for the problematic interaction between the render system and the theme system when using #pre_render, which has been extremely problematic for some time now.
Patch is against
71287c867fc06483b2f0b73b2241efa4842102f6
, from now 10 days ago. Hence marked as "do not test".Attached patch still needs quite a bit of cleanup, but I hope that by reading
MenuTreeInterface
andMenuTree
, the approach is clear (and if not, see the above, that should help also).This patch converts all rendered menus:
Into the exact same pipeline. It makes all of them cacheable.
It also fixes the critical regression of dynamic access checking no longer happening in menu blocks. As of #2158003: Remove Block Cache API in favor of blocks returning #cache with cache tags, menu blocks are cached per role. Which means that any menu link that needs dynamic access checks (e.g. "view own unpublished content") simply is cached for the first user with a given role.
This also removes all caching in the
MenuTree::build*()
functions — we now rely solely on render caching, plus of course the cached versions of "access cacheability analysis" for each menu tree. Overall, this allows us to use caches much more effectively.Good news is that e.g. the admin menu has ZERO non-role cacheable access checks by default (and probably nearly always). Which also means that we'll be able to change the toolbar tray caching to be per-role :) (or even better: detect whether it should be cached per user or per role).
Finally, note that menu blocks in their "Cache settings" now tell you which menu links are costly. This needs UX love, and is questionable (because scary), but at least this is now possible.
Benchmarks
Note:
(Measured using the excellent https://drupal.org/project/webprofiler), using PHP 5.5 with OpCache enabled, XDebug disabled.)
Default menu
The default menu, with 1 link: "Home" (which is role-dependent).
/
/contact
Typical menu
I configured the "main menu" with 3 links: 3 role-dependent ones ("Home", "Contact", "Admin").
/
/contact
Typical complex menu
I configured the "main menu" with 6 links: 3 role-dependent ones ("Home", "Contact", "Admin") and 3 non-role-dependent ones (to nodes 1, 2, 3).
/
/contact
Profiling
I profiled the typical complex menu scenario with XHProf, with the following results:
/
— Before vs. After/contact
— Before vs. After/
— BeforeWrong vs. After/contact
— BeforeWrong vs. Afteri.e. 12–20K less function calls per page load (out of 80–90K calls per page load).
Comment #98
pwolanin CreditAttribution: pwolanin commentedI'm concerned about the approach in the issue summary. A menu tree can be arbitrarily large. Loading the whole thing and doing later collapsing and filtering is fine for the toolbar, but not for the general use case.
Is the summary up to date?
I'd prefer to discuss the approach here in more detail.
Comment #99
effulgentsia CreditAttribution: effulgentsia commentedNo. It doesn't reflect the patch at all. I was about to add the "Needs issue summary update" tag, except looks like it already has it.
Comment #100
Wim Leers#98: pwolanin: please read #96. I think #96 with some minor adjustments will make for a fine "proposed resolution" section in the issue summary.
Comment #101
msonnabaum CreditAttribution: msonnabaum commented#96 seems reasonable to me, and the patch looks like a solid improvement already. Looking forward to the cleaned up version.
Comment #102
Wim LeersStraight reroll to apply to HEAD. It still appears to be working :)
(#2222719: Use parameter matching via reflection for access checks instead of pulling variables from request attributes landed a few days ago, which made rerolling this more painful than if it were just #2247991: [May 27] Move all module code from …/lib/Drupal/… to …/src/… for PSR-4, which landed yesterday. But it was actually still surprisingly easy, considering this was rolled against a now ~2 weeks old Drupal 8!)
Comment #104
Wim LeersClean-up phase 1. Refactored the determining of 'access check is cacheable per role' to be sane.
Updated the issue summary at a high level. As I clean up different parts of the patch, I'll continue expanding the issue summary.
Comment #106
martin107 CreditAttribution: martin107 commentedPatch no longer applies ..
Comment #107
camprandall CreditAttribution: camprandall commentedI will re-roll this patch.
Comment #108
camprandall CreditAttribution: camprandall commentedI can't find a commit where this patch succeeds so, being a new contributor, I'll let someone else handle this. ;o)
Comment #109
Wim Leers#108: thanks for the attempt; this is definitely a very non-trivial reroll :)
High-level changes in this reroll:
DomainCacheContext
; now included. That was the main cause of the ~6500 test failures.MenuTree(Interface)
is now much more focused: it's only purposes are to build a menu tree and render it.MenuTree::buildAllData()
,MenuTree::buildPageData()
andMenuTree::buildTreeData()
into smaller pieces that are reusable, rather than the same logic being repeated in multiple places with subtle differences. The logic responsible for determining the query to retrieve a menu's menu links according to certain parameters now lives in a newMenuTreeParameters
objectmenu.inc
and partially inMenuTree
(partially in those); it now all lives inMenuDefaultTreeManipulators
MenuActiveTrail(Interface)
MenuTreeItem
value object has been added, to make the resulting menu tree (ofMenuTreeInterface::build()
) more well-defined/formalized and hence more reliable/less brittle. A menu tree now consists of menu tree items, just like before, but instead of it being a particularly structured array, it's now a tree ofMenuTreeItem
s. Menu tree manipulators receive and manipulate these.expanded
flag enabled are not expanded if the current response is a 404, is performance: that used to cause a cache miss, but thanks to per-active trail caching, that's no longer a cost. So we don't need to special case it anymore, especially not because this "optimization" actually hurts performance now :)MenuLinkStorage::loadModuleAdminTasks()
is obsolete now, so it's been removed.Next steps
Getting both of those in separately will already make this patch much more digestable. Once those have landed, I'll split off more parts if desired.
Apologies
… for posting such a huge patch. I indicated earlier that I wanted to update the patch (and the issue summary) in phases, but it turned out to be impossible, because every single part I tried to change caused ripple effects everywhere else: the current ingrokkable, convoluted spaghetti code base made step-by-step cleanups infeasible. That's also why I wrote unit tests for All The Things: so that this patch becomes easier to iterate, and so that once it lands, future bug fixes, regression tests, feature additions and refactorings become significantly easier.
Also apologies because I've been under the radar for 2 weeks, but if I'd been posting patches that are impossible to follow without unit test coverage, that would also only have been an annoying distraction. I felt my time would be best spent presenting a solid patch, rather than a series of rough patches.
There's still room for improvement, of course, but now it's been proven to work. If you think there's an edge case that's uncovered, please just add a new unit test case that fails :)
Comment #110
Wim Leersd.o--
Comment #112
Wim LeersI'm on PHP 5.5. That's probably why I didn't notice that pesky invalid syntax problem :)
Comment #113
Wim Leerstestbot's not picking up #112. Reuploading.
Comment #115
Wim LeersThis should fix all fails except for those in
Drupal\rest\Tests\CsrfTest
. How the changes in this patch can breakX-CSRF-Token
header authentication, is beyond me. If someone else knows how to efficiently debug that, please do so.Comment #117
effulgentsia CreditAttribution: effulgentsia commentedSimple fix :)
Comment #118
Wim LeersHAHA :D Thanks, Alex :)
#2247779: Allow #pre_render, #post_render and #post_render_cache callbacks to be service methods landed, which was also included in this patch. Removing that from this patch.
The next step (working on that right now) is to move the addition of cacheability metadata (see #104) into a separate issue.
Comment #119
Wim LeersAccess checker cacheability metadata has been split off into its own patch now: #2287071: Add cacheability metadata to access checks.
This patch is still ready for reviews though — just feel free to ignore the changes to access checks!
Comment #120
Anonymous (not verified) CreditAttribution: Anonymous commentedi like the look of this patch. only thing that stood out was this:
- the comment is redundant
- more importantly, it looks like the order of the keys could vary, so we prolly want to sort before implode...
Comment #121
msonnabaum CreditAttribution: msonnabaum commented1. The handling of primary/seconday menus is obviously not very DRY, but assuming https://drupal.org/node/1869476 goes in as the comment suggests, that's a non-issue.
2. The naming here is awkward, and I had to dig into the code to figure out what was going on:
I think the source of the awkwardness is that the method is on $cacheable_access_check, so it doesn't read quite right. willBeEmptyForAccount would be a fine method name if it was on something like MenuTree, but here, it would probably work better as something like willMenuBeEmptyForAccount.
3. It took me a while to get the whole menu tree manipulator concept, but it still looks considerably simpler than what was there before. A few things threw me, but I think they are fixable.
Using the servicename:method notation with two different services makes me think that this is an extensible system, where contrib could define new services to be added to menus in core. I don't think this is the case, which makes sense, but I think we can suggest it more in code.
I'd prefer to see all the manipulator methods on DefaultMenuTreeManipulators, regardless of who is doing the work. We'd just need a proxy for checkNonDynamicAccess:
Then the previous section could be simplified to:
4. This class name is awkward, as I'm not sure whether "Check" is a verb or a noun until I read the whole thing, but it is accurate.
Maybe we could do this instead?
That said, the docs on this class are *amazing*.
Overall, this patch is looking great. Huge step forward.
Comment #122
effulgentsia CreditAttribution: effulgentsia commentedI discussed this patch with Wim and feel like some of the caching related logic and APIs (e.g., #2287071: Add cacheability metadata to access checks) still need a fair amount of review and polish before being committable. However, this patch also includes a ton of general menu rendering refactoring and test coverage. I think it would be good to get that part in separately for two reasons:
1) I think a fair amount of that will conflict with #2256521: [META] New plan, Phase 2: Implement menu links as plugins, including static admin links and views, and custom links with menu_link_content entity, all managed via menu_ui module, and Wim would prefer to get the test coverage in first, and then git merge into that issue with this coverage available for helping resolve conflicts, rather than the reverse order. But I don't want that issue held up on getting all the caching related stuff finished.
2) The caching logic here isn't trivial, so getting the other refactors in first and out of this patch will facilitate the review process on the caching logic.
Therefore, I recommended to Wim that he open a new issue titled something like "Refactor MenuTree to be simpler and have full unit test coverage" for getting that initial piece in.
Comment #123
Wim Leers#122: done: #2289033: Refactor MenuTree to be simpler and have full unit test coverage.
That, together with #2287071: Add cacheability metadata to access checks, will allow this patch to become much more reviewable. It should end up in the sub 100K range, probably 50–70K.
#121: thank you so much for the review! :) Points 1, 2 and 4 apply only to the render caching that's being introduced here. Your 3rd point applies partially to #2289033 now, but the second half applies to this issue. I addressed the first half of point 3 at #2289033-3: Refactor MenuTree to be simpler and have full unit test coverage.
Comment #124
pwolanin CreditAttribution: pwolanin commentedThe conflict with #2256521: [META] New plan, Phase 2: Implement menu links as plugins, including static admin links and views, and custom links with menu_link_content entity, all managed via menu_ui module is possibly very fundamental in terms of this approach of "manipulators". I think Wim and I need to talk to figure out a common plan.
Comment #125
Wim LeersSiamese twin comments were posted to #2289033-8: Refactor MenuTree to be simpler and have full unit test coverage and #2256521-75: [META] New plan, Phase 2: Implement menu links as plugins, including static admin links and views, and custom links with menu_link_content entity, all managed via menu_ui module, respectively. In short: pwolanin and I have had a long discussion today on how to move forward; the conclusion is that #2289033 (my issue) will be closed and will be combined with #2256521 (pwolanin's issue).
So, that's my next big focus: helping bring that to completion. Once that's done, and perhaps in between, I'll continue working on #2287071, which has been hugely expanded in scope on effulgentsia's request.
I'll update the issue summary of this issue as soon as either of those land.
Comment #126
Wim LeersAll 5 parts of #2256521: [META] New plan, Phase 2: Implement menu links as plugins, including static admin links and views, and custom links with menu_link_content entity, all managed via menu_ui module have been committed by now. Which means that #2289033: Refactor MenuTree to be simpler and have full unit test coverage has been committed as part of that, though in slightly changed form.
The only other blocker for this issue is #2287071: Add cacheability metadata to access checks.
I've been working on that one for the past week, and I just posted the first patch there that's actually reviewable: #2287071-15: Add cacheability metadata to access checks.
effulgentsia insisted in that issue that we widen the scope to fully resolve the lack of access check result cacheability metadata, so including the entity access hooks (that's why it took so long to get a reviewable patch going). While this is the cause of the significant increase of work and patch size, at the same time that will allow us to cache almost every menu link, because we will then finally know whether e.g. a node is accessible and that this result is valid per role, or per language, or per role and per language, or per group, or … In other words: we will finally no longer need to treat entity access checks as a "black box", of which the results are per definition uncacheable. That should mean that this patch will become even more effective! :)
Comment #127
catchI'm not clear why #2287071: Add cacheability metadata to access checks blocks this as opposed to allowing a further optimization?
Comment #128
Wim Leers#127: I just replied to your comment in #2287071: Add cacheability metadata to access checks, perhaps it's clear after reading that?
Comment #129
Wim LeersWhile working on this, it became clear that #2273277: Figure out a solution for the problematic interaction between the render system and the theme system when using #pre_render needs to be fixed in order for this issue to be fixed. I now have a reviewable (and hopefully RTBCable) patch there. #2287071: Add cacheability metadata to access checks has also been pushed forward significantly in the past few weeks. I'm blocked on reviews on both of those.
This patch is unnecessarily complicated by the fact that primary and secondary menu links still are special snowflakes with their own rendering pipeline dating back to Drupal <=5 — they're rendered inside the theme preprocessing layer rather than as blocks. #474004: Add options to system menu block so primary and secondary menus can be blocks rather than variables and #1869476: Convert global menus (primary links, secondary links) into blocks will fix that.
So, the effective list of blockers is now:
(I'm leading the first two and helping with the last two.)
I'm currently working on a reroll of this that's based on top of the latest patches in #2287071 and #2273277.
Comment #130
Wim LeersIn the mean time, #2273277: Figure out a solution for the problematic interaction between the render system and the theme system when using #pre_render has landed, making this issue only blocked on a single remaining issue: #2287071: Add cacheability metadata to access checks. That one is also very close. I've been working on rerolling this patch already :)
There's also news for the two soft blockers: #474004: Add options to system menu block so primary and secondary menus can be blocks rather than variables landed today, and I rolled an initial (rough, incomplete) patch for #1869476: Convert global menus (primary links, secondary links) into blocks. The former already having landed doesn't help this issue at all, but hopefully the second will land, which will greatly simplify the work for this issue.
Comment #131
Wim LeersThe last patch that "completely" addressed this issue did not yet address the case of "dynamic menu links": menu links where the link title is dynamic, or it has a
?destination=<current_path>
query string, a session token, or a CSRF token.Since #2256521: [META] New plan, Phase 2: Implement menu links as plugins, including static admin links and views, and custom links with menu_link_content entity, all managed via menu_ui module, we now have
MenuLinkInterface::isCacheable()
that gives us that information. Such dynamic links can not be render cached. The only way we can render them is either through JavaScript or through#post_render_cache
, otherwise they'd "infect" a render cached menu. They're rare enough that it isn't worthwhile to spend the time building a JavaScript-based solution which is potentially more efficient. Therefore, we must render them via#post_render_cache
.So, we must be able to rely on the return value of
isCacheable()
, or otherwise we'll end up render caching e.g. link with CSRF tokens. Turns out that we currently cannot rely onisCacheable()
, indeed causing render cached CSRF tokens. I've opened #2335661: Outbound path & route processors must specify cacheability metadata to fix that, and have posted an initial patch.Comment #132
Wim LeersWoot, #2287071: Add cacheability metadata to access checks just landed!
Comment #133
Fabianx CreditAttribution: Fabianx commentedI opened #2351015: URL generation does not bubble cache contexts as an alternate approach to unpostpone this issue.
Comment #134
Fabianx CreditAttribution: Fabianx commentedComment #135
ianthomas_ukAdded new blockers to issue summary
Comment #136
xjmComment #137
webchickI'm not sure but I believe that this issue is now postponed on #2351015: URL generation does not bubble cache contexts so noting this in the issue summary.
Comment #138
catchIt is, however we discussed this a bit last week
1. 99.9% of menu links don't actually use CSRF protection.
2. There's no hard code dependency between the two issues.
So given the menu link CSRF protection issue is critical, I think it'd be OK to un-postpone this and handle them in parallel.
Comment #139
catchActually un-postponing this time.
Comment #140
webchickComment #141
Wim LeersFrom the #drupal-performance IRC channel, March 10, 2015:
So, basically postponing this on #2429287: [meta] Finalize the cache contexts API & DX/usage, enable a leap forward in performance. See the Google doc in that IS for details.
Comment #142
effulgentsia CreditAttribution: effulgentsia commentedThat meta issue has child issues, so this title should probably reflect a larger number, but until we figure out exactly which children are blockers for this, just putting "1" into the title for now.
Comment #143
webchickAccording to the issue summary of #2335661: Outbound path & route processors must specify cacheability metadata that is also blocking this one.
Comment #144
Wim LeersJust #2335661: Outbound path & route processors must specify cacheability metadata already presents a big step forward: it makes it so that the generated links themselves have the right cacheability metadata. That's the biggest blocker to this issue. And that patch is green and close to RTBC.
From the work I had lying around from long ago, I also split off #2479363: Cache MenuActiveTrail::getActiveIds() for *all* menus per route match: 1 cache get instead of N DB queries, saves 1 ms/response into a child issue already. That removes a few unnecessary queries.
After that, there are basically two problems left:
But… in reality:
In other words: the 99.99% case — and that's not an exaggeration — doesn't have uncacheable links or access checks in menus. The use case of menu links being hyperdynamic (the "Hello, Wim" use case) works just fine too: they'd be cached per user, which would mean the menu is cached per user. Sites that want to do that sort of thing therefore will just have menus that are cached per-user. That's fine: only those sites will have that problem.
That allows us to move forward with a solution for the 99.99% (i.e. this is then no longer postponed on #2429287: [meta] Finalize the cache contexts API & DX/usage, enable a leap forward in performance as described in #141). The remaining 0.01% can happen in 8.x.0 without an API break, or in contrib: it is possible to placeholder the uncacheable parts and still cache the cacheable portions of a rendered menu. But it will make the code significantly more complex. The analysis I made in #141 is incorrect; it oversimplifies things: the tricky thing with menus is that child menu items affect the parent menu item: the
expanded
andcollapsed
classes.Note again that HEAD IS BROKEN: it caches menu blocks, but it actually doesn't know how: therefore it may show links to a user that should not even be able to see them. So, below, "HEAD-fixed" is HEAD with the menu block no longer cached (by adding
getCacheMaxAge() { return 0; }
.This is profiled as UID 1 on the frontpage, using the Standard install profile. Zero extra menu links, so only the "Add content" menu link. UID 1 because that is *cheaper* in terms of access checking (lots of things return early for UID 1) and because that user can access the toolbar.
In other words: the numbers below present the minimum expected improvement, for more complex set-ups and for non-admin users, the improvement will be much bigger. (See #97 for an indication of the performance improvements in other scenarios; they should be roughly comparable still.
Profiling (XHProf)
Benchmarking (
ab -n 1000 -c 1
)HEAD vs. patch
(i.e. fast-but-broken HEAD)
HEAD-fixed vs. patch
(i.e. slower-but-correct HEAD)
The included patches are:
As you can see, the actual patch is only ~20 KB!
Comment #146
Wim LeersSo, the failures in:
PageCacheTagsIntegrationTest
: one small oversight (user.roles
is no longer present, becauseSystemMenuBlock
no longer stupidly hardcodes it) and one actual bug (the changes I made inBlockViewBuilder
), because I should've just updated the existing logic rather than adding my own.DefaultMenuLinkTreeManipulatorsTest
were due to a silly bug in this patch, now fixedFieldWebTest
were due to #2335661-108: Outbound path & route processors must specify cacheability metadata, now fixed in #2335661-112: Outbound path & route processors must specify cacheability metadata, so that patch is now includedTherefore, the included patches are now:
Comment #147
Wim LeersAs I said in #146, this patch still needed to be updated to support #2479767-12: Fix MenuLinkInterface::isCacheable(): remove it in favor of implementing CacheableDependencyInterface, which keeps the ability for links to have dynamic text/URL, but makes the support for that sane. This reroll adds support for such dynamic links to
MenuLinkTree::build()
, and therefore to menu tree render caching.Comment #150
Wim LeersFixed a small mistake.
Comment #152
Wim LeersComment #154
Wim Leers#2479363: Cache MenuActiveTrail::getActiveIds() for *all* menus per route match: 1 cache get instead of N DB queries, saves 1 ms/response and #2479767: Fix MenuLinkInterface::isCacheable(): remove it in favor of implementing CacheableDependencyInterface both landed. Only #2335661: Outbound path & route processors must specify cacheability metadata still needs to land now.
Working on fixing the failures.
Comment #155
Wim LeersThe failures in #152 were most welcome: they pointed out:
#cache
, to indicate the cacheability of its options. I went with option B. I ensured that users of\Drupal\Core\Menu\MenuParentFormSelectorInterface
do not get broken.Rerolled on top of #2335661-117: Outbound path & route processors must specify cacheability metadata.
Comment #156
Wim LeersNo need to modify toolbar.module here, we have a dedicated issue for that: #2254865: toolbar_pre_render() runs on every page and is responsible for ~15ms/17000 function calls, so reverting those changes. (Note: those changes didn't have any effect anyway, also not in the benchmarks in #144, because the toolbar module is doing some very complex trickery.)
Comment #157
Wim LeersComment #158
Wim LeersTwo
@todo
s remain, but neither should be solved in this issue, IMO. Opened #2483181: Make book navigation block cacheable and #2483183: Make breadcrumb block cacheable, including explanations for splitting it off.So updating those @todos to those new issues, to keep the scope of this issue manageable.
Comment #159
dawehnerSo we do that in order to avoid whether $tree[$key]->access exists? From an API POV I could imagine creating a tree to render things without carrying about access in the first place.
That is nice!
Well, it used to be FALSE, doesn't it mean we want to apply
::forbidden()
?So all return statements apply cachePerPermissions. What about not doing an early return and apply it at the end? That seems to be possible when we rewrite the code without any additional executed code.
Didn't we tried to decouple menu rendering from its config entity many times already? Do we have to reintroduce it again?
Do we really have to adapt the interface? We seem to not change any of the implementations using that method beside the internal one of the form element.
Should we have some form of helper method for that checking? Seems quite random that this is part of blocks.
It still odd that you need all those three lines. Maybe we could open a follow up to also be able to pass in two render arrays?
Do we mind describing where those are coming from? I could imagine that this eases debugging in the future.
Should we open up a follow up for that?
Comment #160
Wim Leers#2335661: Outbound path & route processors must specify cacheability metadata landed!
I'm gonna work on addressing #160 + test coverage on my flight to LA.
Comment #161
Wim LeersComment #162
Wim LeersFirst, a straight reroll. Hopefully still green.
(Roaming + phone, plane has not yet left.)
Comment #163
Wim Leers#159:
\Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkAccess
. I don't think you're questioning that.The second bit you quoted exists because
\Drupal\menu_link_content\Tests\MenuLinkContentCacheabilityBubblingTest
is an integration test that wants to verify cacheability metadata bubbles correctly. Which means we must render the menu tree. Which is done by\Drupal\Core\Menu\MenuLinkTree::build()
, which indeed always checks$tree[$key]->access
.I agree that from an API POV, it should be possible to create a tree without caring about access… and in that case you would do something like this. The alternative is to let
\Drupal\Core\Menu\MenuLinkTree::build()
happily ignore anyMenuTreeElement
whoseaccess
property is NULL. But then we cannot distinguish access checked from non-access checked elements in the tree.I think this is a reasonable compromise (i.e. requiring access to be set to be allowed on every element in the tree when you don't want to do access checking). I hope you do too.
Whenever we talk about defaults in terms of access results, we want other code — in this case a menu link tree manipulator that runs *after* this one — to be able to override it. "Forbidden" means "we have conclusively determined that this should never ever be accessible, for explicit reasons". Here, there are no explicit reasons.
Therefore, I think "neutral" is more appropriate here. If the argument I just gave doesn't convince you though, then I'm happy to change it. I don't feel strongly about it.
menu_ui.module
. Updating the only other caller (inMenuController
) is currently pointless, since it'd imply making the JSON response cacheable, which is out of scope for this issue. I've added a@todo
to that one.Element::hasOnlyCacheabilityMetadata()
… which is quiet a mouthful. Other suggestions?\Drupal\Core\Render\Renderer::mergeBubbleableMetadata()
for that already. But indeed, out of scope here.Comment #164
jibranInterdiff please. :)
Comment #165
Wim LeersOops.
Comment #166
dawehnerNo, I actually question exactly that ... the API does not force you to add that manipulator, so the building should also not depend on it directly.
Ah right, shit stays shit, I hope this at least doesn't cause failures if a corresponding menu config does not exist.
Can't we call it Element::emptyRenderArray() or something like that.
Comment #167
Wim LeersDiscussed #159.1 & #159.95 with @dawehner in person.
Comment #169
Wim LeersComment #170
Wim LeersThis issue still has the "needs tests" issue tag. Let's analyze what's necessary to be able to remove that.
RendererBubblingTest
and many integration tests.MenuLinkInterface
implementations may return dynamic results, i.e. for dynamic menu link titles or even pointing to different menu links)MenuLinkContentCacheabilityBubblingTest
(which was added in #2335661: Outbound path & route processors must specify cacheability metadata)So, now working on tests coverage for the two aspects that are still lacking test coverage.
Comment #171
dawehnerJust some more small points.
Just for testing for a more realistic usecase it would be nice to add an element with just #markup at some point.
Don't we need an instanceof check here as well?
Comment #172
dawehner.
Comment #173
dawehner@Wim Leers
So are you done with that test coverage or can I help you with the two remaining points?
Comment #174
Wim LeersCurrently writing that test coverage. I'll fix the nits in #171 too, unless you want to do that already (feel free to). Thanks for the review!
Comment #175
Wim LeersHere's the test coverage. Of course, I found two nice bugs in the process :)
MenuLinkTreeElement::access === NULL
, despite that explicitly being the way that dawehner wants to allow people to useMenuLinkTree::build()
to render menu trees without using access checkingNext: addressing #171.
Comment #176
Wim Leers#171:
NULL
. (If::checkAccess()
doesn't reliably set an access result, then we are in big trouble.)Next: IS update.
Comment #177
Fabianx CreditAttribution: Fabianx for Acquia commentedWhy do you need a reference?
The lowest level should never change and I don't see the caller calling this?
Also does this not need an interface change?
So $data->access can never be set to FALSE or such?
Comment #180
Wim Leers#177
$level
parameter only exists on this implementation, not on the interface.AccessResultInterface
instance.Also fixed the hundreds of exceptions.
Comment #181
Wim LeersEt voila, a vastly better issue summary.
Comment #183
Wim LeersFixed the test failures. Had to undo #177.1/#178.1, because merging two cacheability metadata objects results in a new cacheability metadata object.
Comment #184
catchIncomplete sentence.
Would this really be an empty menu for all users, or would just the menu never contain this top level link?
Also, in the case of root menu links we have to keep the link for the cache context bubbling, but all other levels we don't have to keep - because of cache context bubbling. Without going in and debugging, I can't see just from reading the patch through why only the 'root level inaccessible' links are the only ones that have to be kept and the others discarded, just off the top of my head I'd expect it to be the 'top level inaccessible link' at whichever level of the menu hierarchy - since it's the cache context for that which determines whether there's ever an opportunity to view the links below it.
automatically vary?
What's the impact of this?
Nit: dynamism (although that's not a perfect word here, but it is a word :P)
Is the only choice for non-permanent max-age=0 or can you set max-age=86400 or something?
Comment #185
mbrett5062 CreditAttribution: mbrett5062 commentedAt the top of the issue summary it says this is blocked by #2351015 "and" #2335661.
However, in the remaining tasks it says we must land either #2351015 "or" #2335661.
Which is it?
Comment #186
Wim Leers#185: Oops, I intended to remove that in #181, but clearly I failed. Removed now.
#184: Addressing that next.
Comment #187
mbrett5062 CreditAttribution: mbrett5062 commentedThanks @Wim
Comment #188
Wim Leers(Funny enough, precisely this is what point 1 is about — but as you pointed out, the sentence was incomplete.)
Oh my, can't believe I missed that. You're right of course. Fixing that in the next reroll, and will include test coverage.
max-age
value, of course. I merely chose to only vary cache contexts here, and not max-age and tags as well. TheCacheableMetadata
test coverage ensures that all of the bits of cacheability metadata (max-age, contexts, tag) are handled correctly. No need to repeat that here.Comment #189
Wim LeersHere we go.
Comment #191
Wim LeersTestbot, you're dumb. Re-testing.
Comment #194
Wim LeersTestbot, you suck. Re-testing again.
Comment #196
dawehnerWe reviewed it enough over the time. I think we can get this in, finally
Comment #197
webchickThis one could definitely use catch-like eyeballs, methinks.
Comment #198
Fabianx CreditAttribution: Fabianx for Acquia commentedOverall RTBC + 1, just did a very thorough review again.
My main concern being $access change from bool to AccessResultInterface and not having a CR nor a API change tag for that.
Also very unclear of how $access worked before and how that code is now gonna deal with having an object in there.
Not affecting RTBC status:
If the interface says public function build() I expect this to be used plus an internal protected function:
protected function recursiveBuild(array $tree, $level, CacheableMetadata &$tree_access_cacheability = NULL, ...) {
}
It is an implementation detail and using default arguments, while it works needs some special cases for level === 0, which would be totally unnecessary with the helper function - hence making the code simpler and always returning $items from recursiveBuild and a render array from build().
- Would love to see as follow-up.
Just a question:
If something sets $data->access to FALSE (say I copied code from node before this change) I think then we would hide the link by default for this circumstances, but because no cacheable metadata bubbles up, then it could suddenly be available for me, right?
Should we not assert strongly (well exception) that its either NULL or TRUE, but not FALSE?
And if it is FALSE at least add a max-age=0 or throw an exception?
I assume something deep down checked for if ($data->access === FALSE) before or such? Or how did access checking prior to this work?
Nice, here the work to check accessible-from-the-top pays off.
If we move it (in a follow-up) to its own helper function, can probably even remove the $level parameter.
Note to committers:
Commit conflict with the config:system.menu to menu:[menu-name] cache tag conversion.
Probably this patch would fail if the other was committed first, but alerting in any case ...
Necessary API change, still think we need to at least add an Exception in case someone uses the old interface or does it D7 style by habit ...
=> Information disclosure prevention
=> can be follow-up though
Tricky question:
Is anything in here allowing to change $element via a hook or something?
If not we are fine ...
Immutable objects have interesting patterns ...
Could ->access be NULL or FALSE here and not an object?
How do we prevent that?
What about #post_render_cache and #attached?
Are we not loosing data that way atm.?
It is an edge-case, but still asking ...
--
O ic, in that case we would treat it as non-empty, which would render the block, which is fine ...
Oh, why do we do that?
Have to check the issue for that ...
Only slightly related, but what happens if an editor/admin with more rights selected a link and an editor with less rights then comes and tries to save that form? Invalid choice?
So we currently theoretically disclose information as that JsonResponse is (?) cached by page cache, but necessary contexts are not available?
Isn't that a rather large UI/UX change (do we have product manager sign-off on that?) that I can no longer access all menus / menu items within the admin?
Have to find where this was discussed in here.
--
Also: Isn't that change out of scope here?
Yeah!
Again, is this in scope? Why do we change this here?
Yeah!
Are those the changes for it before being ->access as a boolean and now being an AccessResultInterface?
Should not the code that before checked $access do that?
Comment #199
webchickBonk.
Comment #200
Wim Leers\Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkAccess
'sif (!isset($element->access))
.::getParentSelectOptions()
always uses the::checkNodeAccess()
+::checkAccess()
menu link tree manipulators. That means this code will always receiveMenuLinkTreeElement
objects with theaccess
property set toAccessResultInterface
objects.#cache
to specify the reasons for its emptiness, it is considered empty.AccessResultInterface
objects asMenuLinkTreeElement::access
values. But that's no longer true! Good catch :)So reverting all changes in this file; tests should still pass. That's another piece of proof that dawehner's request/requirement to have
MenuLinkTree::build()
also work on non-access checked trees, still works.\Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkAccess()
itself already removed inaccessible links! That, however, was very, very, very wrong/problematic: it prevented the rendering code to know the cacheability metadata of inaccessible links. Which meant that we only knew the cacheability metadata of accessible links. Which of course is insufficient/incomplete knowledge. And could result in some branches of a menu never becoming accessible, because we didn't know about the cache context that would make that link accessible to other users!So, good review, but fortunately lots of things that have been answered before. It's good that you now have full clarity on them.
So, only 2 changes in this patch:
MenuLinkTree::build()
a bit, so that it doesn't add optional parameters relative to what the interface says, and to have easier to understand codeMenuLinkContentCacheabilityBubblingTest
Comment #201
larowlanLooking good, just a couple of questions/comments
$tree_link_cacheability is already a CacheableMetadata object - any reason why we use createFromObject again? If so could we expand the comment.
%/still will/will still/?
Should we wrap the second condition in () to make it clear the order here?
Any reason for the asterisks here, we don't normally do that?
Comment #202
Fabianx CreditAttribution: Fabianx for Acquia commentedThanks for the great response!
- Tagging API change
- This needs a change record for the API break (I agree the API change is more than necessary)
- It would be great to have a quick beta evaluation, too
- This needs also a change record and an issue summary update for the way more subtle, but important part that you know need to check access yourself (!) after using the default menu tree manipulators.
And this is where I am unfortunately still a little unhappy.
But given we don't have anyway to store cacheability data on something like the root of the tree, this is likely still the best option to keep the element.
However:
Don't we have a NULL link / Menu Element we could set to prevent ever exposing the path / menu text accidentally? (to prevent new information disclosure)
It feels to me that the caller needs to check access is acceptable, but only if we ensure we don't expose the underlying data.
So lets say we would have:
Before:
After:
Don't know too much about the data structures used there, but that would be my gut feeling how to prevent this API change from causing any information disclosure down the road.
If we feel InvalidLinkAccessLink() is too complicated, could also be an empty link or whatever makes the most sense ...
Comment #203
Fabianx CreditAttribution: Fabianx for Acquia commentedAnd another nit-review with mostly optional changes:
nit - $level is no longer used
That comment does no longer make as much sense as the behavior is the same for every level.
nit - $level is unused now.
nit nit nit - we want to do the same in the if/else case, could theoretically simplify the code here.
Comment #204
jibranWhy would we want that? This is critical issue related to performance. It is already triaged by commiters.
Comment #205
Fabianx CreditAttribution: Fabianx for Acquia commentedBecause of the disruption section.
Comment #206
Wim Leers#201:
RE: unhappiness. There are many factors significantly diminishing the risk:
MenuLinkTree::build()
. And hence be safe.MenuLinkTreeElement::access
property. It already exists in HEAD. It was just being strangely abused/underused by the the default menu link tree access checking logic, solely for legacy reasons. It's there, and it's clearly documented. Any half-assed QA testing would cause the developer to discover that inaccessible links are being rendered.I think that's more than enough risk limiting. The only way we can every truly prevent any problems ever here, is by somehow disallowing menus to be rendered manually. Which we cannot prevent.
On top of that, your proposal doesn't make sense, because it is acceptable to not run access checking: if
MenuLinkTreeElement === NULL
, then no access checking happened, and we just render the entire tree. @dawehner very, very strongly felt that it was important we don't require access checking to be able to render a menu tree. See #159.1 + #166.1.#203:
Rebased; there were a few conflicts caused by #2491155: Update drupal.org and kernel.org URLs in core modules (Follow-up to 2489912).
Comment #207
Wim LeersDiscussed further with Fabianx & catch, they feel quite strongly about the thing Fabianx proposed in #202:
So, implemented that. Found a way to minimize the DX damage, i.e. by still allowing people debugging code to see which link is actually the inaccessible one.
Comment #208
dawehnerMh, doesn't that adds quite some memory for users without access? I'm especially thinking in terms of #2250315: Regression: Bring back node access optimization to menu trees
Comment #209
alexpottAdding dawehner, catch, larowlan and fabianx to the credit section.
Comment #210
Wim Leers#208: Yes, it'd increase memory usage a bit, but not a whole lot, because only the top-level inaccessible links get this treatment. Deeper links are still removed altogether.
Fabianx pointed out a silly oversight in #207. Fixing.
Comment #211
Wim LeersI think we should also give commit credit to sun for comment #14, which was hugely important.
Comment #212
alexpottAdding sun to the credit section.
Comment #213
Fabianx CreditAttribution: Fabianx for Acquia commentedThis looks very great to me now!
Thanks for taking care of my concerns:
- Issue summary, change record, beta eval look very great
- All feedback has been addressed
Back to RTBC!
Comment #214
Fabianx CreditAttribution: Fabianx for Acquia commentedComment #215
dawehnerAh good to know, thank you!
RTBC+1
Comment #216
jibranHuh!!! I posted the same point before but couldn't find it now so adding back again.
$build is empty and we are adding the cache meta data anyway. Wouldn't it make more sense to add it in `if` when we actually have a render array? Or before returning from the function if we want to add it anyway?
Comment #217
Wim LeersThat's very much intentional: when the rendered menu — in a block or elsewhere — is empty, then we don't want to just render cache that empty menu and call it a day: we want to know the cache contexts that explain why it is empty. Simple example: all links in a menu have permission-based access control. As the anonymous user, I will be unable to see any links, and the reason for that is a lack of permissions. So, the render cached empty menu has the
user.permissions
cache context. So that if another user with a different set of permission accesses a page with this menu, (s)he does see the menu links.Comment #218
jibranThis is WTF IMHO because it can be easily missed. Do we always have to mention a cache matedata for a render element whether it's showing on the page for current user or not? Or is it a special case for menu.
But, I do agree with this.
Thanks for the explanation.
Comment #219
Wim LeersIf the emptiness is conditional and it is render cached then it's required. Otherwise, we'd end up sending the render cached version (the empty string) to all users (anon, auth, admin, all).
Comment #220
dawehnerThat is a thing used everywhere now, or at least should better be done, as otherwise you have easily potential security holes by not having the required cache contexts.
Comment #222
catchThis should be OK for security.
User 1 has access - stuff is rendered - cache contexts are bubbled.
User 2 does not have access - stuff is not rendered - cache contexts are not bubbled.
It would be possible for 1 to see content meant for user 2, but not for user 2 to see content for user 1. So there's a bug, but no information disclosure.
Patch looks great now. Issue is almost as old as Drupal 8 itself.
Committed/pushed to 8.0.x, thanks!
Comment #223
Wim LeersPublished the CR: https://www.drupal.org/node/2494927.
Commented on #2488918: Use 'menu:<menu name>' as cache tag for menus instead of 'config:system.menu:<menu name>' to avoid coupling, which now no longer conflicts.
Unpostponed #2254865: toolbar_pre_render() runs on every page and is responsible for ~15ms/17000 function calls, #2483181: Make book navigation block cacheable and #2483183: Make breadcrumb block cacheable.
Comment #224
Wim LeersI was also able to close #2463659: Regression test coverage: integration test for an uncacheable menu link that depends on session data and #2268971: Helper for #1805054.
Comment #225
Wim LeersIf you want to help out with making menus and cacheability better in D8, then hopefully I will see you fine folks in
I've either provided patches, or initial patches in all of them. Please check them out, and help if you're interested :) Thanks!
Comment #226
Wim Leers#2254865: toolbar_pre_render() runs on every page and is responsible for ~15ms/17000 function calls landed already, yay! :)
Another issue that is now unblocked: #2217985: Replace the custom menu caching strategy in Toolbar with Core's standard caching..