On a vanilla D7 install, _menu_navigation_links_rebuild() causes the following database queries:

menu_link_save() - 274 calls to db_query()
_menu_link_find_parent() - 264 calls to db_query()
_menu_navigation_links_rebuild - 274 calls to SelectQuery::execute
menu_link_children_relative_depth - 24 calls to SelectQuery::execute

This is all without actually saving the link which is already optimized for, these are all called before we're able to make that decision.

The following patch adds two optional arguments to menu_save_item(), both intended for use only from _menu_navigation_links_rebuild().

The first is the $existing_item that we already load in _menu_navigation_links_rebuild(), which is almost immediately queried again with more fields in menu_save_item().

The second is the overall $menu_links array - since parents are built first, we can look through that for current item's parent ID instead of querying it from the database.

This reduces total queries (to the front page with a forced rebuild) from 983 to 447. This is the most conservative improvement we'll get given real sites will have more modules and links.

It also reduces memory usage for that page from 23mb to 12mb on my machine - most of this through not having to invoke dbntg/PDO so much. Page execution time was reduced from 1700ms seconds to 900ms.

Patch and xhprof screenshots attached (edit: 26.png is 'after', 27.png is 'before').

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Status: Needs review » Needs work

The last submitted patch, menu_rebuild.patch, failed testing.

catch’s picture

Status: Needs work » Needs review
FileSize
3.76 KB

This fixed some of the test failures locally, the other tests are taking so long to run on my machine I've run out of patience.

catch’s picture

Fixing one small bug with $existing_item['options'];

Status: Needs review » Needs work

The last submitted patch, menu_navigation_links_1010480.patch, failed testing.

catch’s picture

Status: Needs work » Needs review
FileSize
3.42 KB

Minus the syntax error.

pwolanin’s picture

Status: Needs review » Needs work

I don't see that the new 3rd param to menu_link_save() is used any place.

catch’s picture

Status: Needs work » Needs review
FileSize
3.78 KB

That's because I'm an idiot and uploaded the wrong patch. This should be the right patch but it hasn't had a test run yet.

catch’s picture

Fixes one notice when items in the $menu_links array don't already have an mlid.

Updated xhprof output, I think the previous one was a cache miss in APC or something, since I'm now seeing a 1.2mb memory saving, not 12mb as previously.

Damien Tournoud’s picture

     // Make sure no child comes before its parent.
     array_multisort($sort, SORT_NUMERIC, $menu_links);
 
-    foreach ($menu_links as $item) {
+    foreach ($menu_links as &$item) {
       $existing_item = db_select('menu_links')

...

       }
       if (!$existing_item || !$existing_item['customized']) {
         $item = _menu_link_build($item);
-        menu_link_save($item);
+        menu_link_save($item, $existing_item, $menu_links);
       }
     }
   }

vs:

+      foreach ($menu_links as $link) {
+        if (isset($link['mlid']) && $link['mlid'] == $mlid) {
+          $parent = $link;
+        }
+      }

The only items in $menu_links that can possibly have a mlid are those that have already been passed through menu_link_save(). As a consequence, it would make more sense to me to build a separate array keyed by mlid and avoid the expensive scan.

catch’s picture

And without the crud. Sorry for rapid fire patch uploads.

catch’s picture

I was thinking about the separate array but didn't want to risk bloating memory with a large number of links. What about just if (!isset($item['mlid'])) { break; } in that foreach?

Menu tests pass locally for me with the latest patch, don't like the look of '0 passes' though.

Damien Tournoud’s picture

@catch: well you can progressively prune the initial array if you like.

catch’s picture

How could we prune it though? Multiple links can share the same parent, so we can't just remove items as they're found. Or did you mean something else?

Damien Tournoud’s picture

I meant pruning the links from $menu_links when we have processed them. Also, it might make sense to *also* keep the customized links:

       if (!$existing_item || !$existing_item['customized']) {
         $item = _menu_link_build($item);
-        menu_link_save($item);
+        menu_link_save($item, $existing_item, $menu_links);
       }

... as this doesn't currently update $item with its mlid if an existing item exists and is customized.

catch’s picture

Oh I see what you mean now, prune the old array while building the new one, OK. I'm out of time for tonight but will have a look at that tomorrow. Vanilla install doesn't have any customized links so we may well get an improvement from keeping those two with actual data sets.

carlos8f’s picture

subscribing

catch’s picture

Building that extra array array cuts 46ms from _menu_link_find_parent() on my machine. Slight improvement, definitely no regression in memory usage.

Before: 47,208 / 6%
After: 1,474 / 0.2%

Query count is the same (actually 2 less but didn't look to see where those are from so might be something else happening).

catch’s picture

People in irc didn't like the concise version of $existing item, so reverted that change. Code style rather than functional change.

moshe weitzman’s picture

Looks reasonable to me. We're gonna need donquixote or chx or pwolanin here I think.

pwolanin’s picture

I'll take a look at it later today - maybe see you tonight Moshe.

pwolanin’s picture

@catch - query count is the same as the prior patch, or the same as without the patch? It seems like the patch must result in a significant reduction in queries compared to unpatched core.

catch’s picture

@pwolanin - same as the prior patch.

pwolanin’s picture

Status: Needs review » Reviewed & tested by the community

The logic of the code looks good to me.

catch’s picture

Andrew Answer’s picture

Can you backport this patch to 6.20 here http://drupal.org/node/593682 ?

mikeytown2’s picture

subscribe

donquixote’s picture

@Moshe (#23):
This is buried too deep in my head atm to review.
Still, I definitely want to be subscribed.

donquixote’s picture

Looking at it:
I initially had the idea that we would totally bypass menu_link_save(), and have some kind of chunked bulk update instead.
That said, if this rather simple patch can significantly speed things up (which I have not tested yet), then I'm all for it.

I wonder if it can be backported to D6.

[side note]
For my own sites I was already thinking about skipping this part of menu rebuild (which would be a core hack, yes). I don't need the navigation menu, and I have my own solution for admin menu. I wonder if _menu_navigation_links_rebuild() is necessary for anything else? All other menus' items are considered "customized", so _menu_navigation_links_rebuild() leaves them untouched - afaik.
[/side note]

catch’s picture

I haven't looked a the possibility of backporting this to D6, but that ought to be doable (although IME performance issues are unlikely to be committed to D6 at this point, but maintaining patches in the queue and submitting them to pressflow is worthwhile).

Marked #593682: Optimize _menu_navigation_links_rebuild() as duplicate - was the older issue, but did not have a Drupal 7 patch.

Andrew Answer’s picture

@catch: what do you suggest? No way to speed up admin UI on thousands D6 existing installations? Bad. If donquixote make something viable my big thanks to him!

catch’s picture

@Andrew, if this patch gets into D7, then it's worth trying to backport the patch to D6 - but Drupal 6 has no automated testing, so issues like this which only help performance but nothing else are less likely to get in than straight bugfixes - every patch committed risks unintentionally breaking something else. However that doesn't stop you applying the patch to your own D6 install, or someone submitting it to Pressflow to try to get it incorporated there. It might end up going into Drupal 6, that's not up to me, but those are the options if it doesn't.

effulgentsia’s picture

subscribe

Andrew Answer’s picture

@catch, the best way I see to help developing is to implement automated testing system for D6 too :) - at least I know what SimpleTest module already backported to D6 in Pressflow. Probably testing architecture much complex than I know, but anyway this is a very good thing for all D6 users, much better than immediate upgrade to D7. I think, users will send D6 patches at least a year more.
You also do not reject patches only because you can't test it automatically. Yes, we can have side effects with these patches, but we can fix it manually... and fix again... and release finally, improving engine at the end. Performance issues ones of important thing for Drupal any version I think.
I create bug in Pressflow's Launchpad for that, but looks like they pre-moderate bugs and decline it by undefined cause. Or I misunderstand his UI :(

carlos8f’s picture

Getting stuff backported to D6 at this stage, if it's not a security bug, is probably impossible :) I haven't had any luck with #235673: Changes to block caching mode not caught. So I just end up applying a handful of patches to my production D6 sites manually.

effulgentsia’s picture

Yes, we can have side effects with these patches, but we can fix it manually... and fix again... and release finally, improving engine at the end.

I disagree with this suggestion. The process of making improvements, discovering side effects, fixing them, and thereby improving Drupal, is what the version of Drupal that is in development is for. Which until recently was Drupal 7, and will soon be Drupal 8. A released and widely used version (e.g., 6) of Drupal core is frozen: minor updates (e.g., 6.20 to 6.21) are for bug fixes that are either critical (e.g., security) or without risk of side effects (e.g., fix a typo).

There may be a lot of people who will continue using Drupal 6 for a while, and they may want performance improvements, but they can get that by patching their installation, or using a distribution like Pressflow.

Creating a backported patch of #22 is worth doing, as is using the issue queue to collaborate, review, and share results about that patch. Anyone is welcome to create such a patch and open a new issue for it, or wait until this patch is committed to D7, and then continue using this issue for the D6 backport. But I don't think it makes sense to expect a D6 backport of this to be committed to CVS, unless it can be sufficiently shown to have 0 side effects. The D6 maintainers, Dries and Gabor, make the final call on what constitutes "sufficient", and they're both very reasonable in taking community input into account.

Every once in a while, people bring up the desire to have more improvements happening in minor updates instead of all being pushed into the next major one. With Drupal 7 released and including good automated test coverage, it's a good time to have that discussion again, with respect to how frozen we want D7 to be now that it's released. But that discussion won't change what we do for D6, and please, let's not have that discussion here. Here's one place: #586146: [policy, no patch] Decide if and how to implement semantic versioning for Drupal 8.x, though if anyone knows of a better one, please add a comment to that issue linking to it.

BTW: I support this patch going into D7, because although it too is an already released version, we've always been a little more liberal in what we allow into the first few minor updates of a new major version.

casey’s picture

Can't we add an _menu_link_save(&$item, $existing_item = array(), $parent_candidates = array())? So we don't alter the API?

catch’s picture

Since it's only adding extra arguments, not changing existing ones, I didn't see any reason to do that. If it's a choice between adding duplicate code and not getting it in at all, then I'd got for the new private function though.

casey’s picture

menu_link_save() could call _menu_link_save(), no need for duplication.

casey’s picture

Another optimization: #1034732: API additions: menu_link_load_multiple() and menu_link_delete_multiple(). that one could use the $existing_item parameter too, in _menu_link_delete_multiple().

catch’s picture

Renee S’s picture

subscribe (I would definitely love to see a D6 version of this, even if it's something that doesn't make it in. Still useful, and as mentioned above, it could be Pressflowed...)

catch’s picture

Version: 7.x-dev » 8.x-dev
Issue tags: +Needs backport to D7
catch’s picture

Issue tags: +Needs backport to D6
catch’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Performance, +Needs backport to D6, +memory, +Needs backport to D7

The last submitted patch, menu_navigation_links_1010480.patch, failed testing.

catch’s picture

Status: Needs work » Needs review
FileSize
4.19 KB

Re-rolled, this had been RTBC for nearly 5 months and no longer applied.

catch’s picture

#50: menu_navigation_links_1010480.patch queued for re-testing.

(sending for retest but I am coming 'round to casey's _menu_link_save() idea so will probably re-roll with that)

catch’s picture

And with _menu_link_save().

catch’s picture

chx reviewed in irc, no functional change but simplified a couple of bits.

donquixote’s picture

I don't want to stop this patch, but I'd like to point out two related things, that might make an even faster nav links rebuild possible.

#512962: Optimize menu_router_build() / _menu_router_save()
The latest patch will make it possible to let the nav links rebuild know which of the router items have changed.
Btw, this thing needs attention / feedback.

#1170278: Introduce hook_menu_router_update()
This would be the mechanism to let nav links rebuild know about the changes.
Depends on the other issue.

Doing this does not mean we no longer want this patch. There might be situations where no router diff is available, or the full router table has changed.

catch’s picture

chx’s picture

Status: Needs review » Needs work

Almost RTBC but

+        $parent_candidates[$item['mlid']] = $item;
+         unset($menu_links[$key]);

that's a whitespace error before unset.

catch’s picture

Status: Needs work » Needs review
FileSize
4.32 KB

Thanks chx, fixed the space.

chx’s picture

Status: Needs review » Reviewed & tested by the community

Well then

webchick’s picture

Status: Reviewed & tested by the community » Fixed

This seems like a great little backwards-compatible performance improvement. Really sorry, I have no idea how it stayed off the radar so long. :(

Committed and pushed to 8.x and 7.x. Thanks!

catch’s picture

Version: 8.x-dev » 6.x-dev
Status: Fixed » Patch (to be ported)

While I doubt it'll get in, I'll probably do a backport for D6 of this if it's straightforward at some point.

deltahex’s picture

Status: Patch (to be ported) » Needs review
hefox’s picture

Confused about the status of this patch; which (if any) is the d6 version of the patch to review?

donquixote’s picture

Some feedback:
I did some xdebug on a D7 test site, with a recent version of D7, and this is still undesirably slow.

drupal_flush_all_caches            = 15,26 seconds
  menu_rebuild                     = 9,33 seconds
    _menu_navigation_links_rebuild = 7,07 seconds
    menu_router_build              = 1,54 seconds
    _menu_router_save              = 0,43 seconds
  [..] (e.g. features_flush_caches     = 2,14 seconds)
  registry_rebuild                   = 2,58 seconds
donquixote’s picture

Re #63
Probably the current solution is still the best that we can get without breaking anything.

------

I am currently working on a rewrite of _menu_navigation_links_rebuild(), which promises to be quite fast. That is, it will really make a difference.

Unfortunately, it might be a bit fragile when backported to D7, due to hook_menu_link_alter().
I have the feeling this is not a very well-designed hook.

The way out:

  • #1170278: Introduce hook_menu_router_update()
    This will allow contrib to kick the smelly core implementation of _menu_navigation_links_rebuild() and replace it with something else, without being overly concerned about compatibility.
  • #512962: Optimize menu_router_build() / _menu_router_save()
    Diff-based rebuild of menu_router, so that the diff can be available to _menu_navigation_links_rebuild().
    I originally thought this should be done before we introduce hook_menu_router_update(), but actually it doesn't matter. We simply extend the signature of hook_menu_router_update(), adding additional arguments for the diff.
  • D8 will revamp all of this with Symfony stuff?
    Well, this might be the case. But we still want these improvements for D7.
    So we should get them into D8 for backport to D7, before Symfony makes all of this unportable.
donquixote’s picture

I am working on a rewrite in a separate issue.
#1830274: Support a parent_path setting in hook_menu(). / hook_menu_link_alter() should not touch the plid.
Speed improvement is between 2x and 3x.

As mentioned, what makes this more difficult is the drupal_alter('menu_link').
This hook itself is ok, but the pain comes when modules alter the plid of the item.

If this stops to happen, we can have a more predictable logic:
- non-customized items always get their parent link id only from hook_menu().
- customized items either keep the parent link they had before, or they move up.

hook_menu_link_alter() should not have a say in this.

kenorb’s picture

Status: Needs review » Fixed

Not much activity here, and Drupal 6 is depreciated.
Fixed in 8.x and 7.x according to #59

kenorb’s picture

David_Rothstein’s picture

Version: 6.x-dev » 7.x-dev

Putting this back to the last fixed version, in that case.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

yonailo’s picture