Suggested commit message

Issue #2301317 by pwolanin, dawehner, Wim Leers, effulgentsia, YesCT, xjm, alexpott: MenuLinkNG part4: Conversion.

Problem/Motivation

see: #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
Next chunk after #2301313: MenuLinkNG part3 (no conversions): MenuLinkContent UI.

Proposed resolution

Parts 1, 2 and 3 added the new code. This part 4 converts core to use it, and converts tests (that were not new tests added in previous parts.) Part 5 #2301319: MenuLinkNG part5: Remove dead code; and party! will remove the code from the old menu links that is not needed anymore and could be a quick commit after this one.
Replace almost entirely the system in HEAD with a new one with minimal user-facing changes but substantial new features under the hood and current and future performance gains (enables render caching).

Overview of the conversion (which includes *using* the new bits from previous parts):

  • Define an extensible framework for different types of links, with a plugin type as a common facade
  • The developer-facing APi is a plugin manager that also handles building trees into render arrays and access checks. The tree storage is a separate service that hides the implementation details of efficient hierarchy handing. tree and tree storage handle menu cacheing.
  • (part 4) adds a new service: menu.rebuild_subscriber MenuRouterRebuildSubscriber
  • Manage links defined in YAML and links defined by views
  • Full l10n support for localizing the D8 admin interface
  • i18n support via config translation for translating the links provided by Views.
  • A NG content entity whose base table stores the plugin definition
  • The custom menu link entity is defined as being fieldable
  • Link title is the (translatable) entity label
  • Menu link content entity can be added/edited via the node form (substituting for existing functionality)

Remaining tasks

Contributor tasks needed
Task Novice task? Contributor instructions Complete?
Update the issue summary No Instructions done
File followup issues for @todo Novice Instructions done
Draft a change record for the API changes Instructions done
Mark existing change records that must be updated Novice done
Manually test the patch Novice Instructions
Review patch to ensure that it fixes the issue, stays within scope, is properly documented, and follows coding standards No Instructions effulgentsia reviewed
Profile, identify regressions, and file followups for optimizations Instructions Done in #7, #16, #27, #28

User interface changes

Minimal, and previously reviewed and documented in part 3: module-defined menu links will have static title, description, and path that are not editable via the UI.

see #2301313: MenuLinkNG part3 (no conversions): MenuLinkContent UI and #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

API changes

Menu links converted to plugins. 3 very notable changes that are included:

  1. All menu links operate behind the facade of the \Drupal\Core\Menu\MenuLinkInterface. This means that multiple different implementations can operate together in the same tree. In addition, since we consider them as plugins, the details of the optimized tree storage are not exposed. The MenuTreeStorageInterface takes only a plugin definition and returns it again, while the details of the implementation (e.g. the p1-p9 columns) are not accessible or part of the API.
  2. The methods for loading and rendering menu link trees have been completely consolidated and made consistent in just 3 methods on the MenuLinkTreeInterface. A lot of previously crufty code that was forward-ported from 6 and 7 has been removed, aided by the decoupling of breadcrumbs form menu links
  3. The code for performing these steps has been broken up into multiple services, each with a clear interface so that in the cases where the behavior needs to be customized for a site or for a different tree storage (e.g. noSQL) only a limited amount of logic needs to be re-implemented to affect the change.

Change record updates

the search: https://www.drupal.org/list-changes/published/drupal?keywords_descriptio...
was done.
Only the following were found to need an update:

CommentFileSizeAuthor
#115 menu issues.PNG80.67 KBcosmicdreams
#98 2301317.97.patch271.31 KBalexpott
#96 2301317-91.patch271.28 KBeffulgentsia
#91 increment.txt610 bytespwolanin
#87 increment.txt1.4 KBpwolanin
#87 2301317-87.patch271.28 KBpwolanin
#84 2301317-82.patch270.95 KBdawehner
#84 interdiff.txt1.42 KBdawehner
#78 increment.txt15.33 KBpwolanin
#78 2301317-78.patch270.35 KBpwolanin
#75 block-translate.png144.74 KBYesCT
#75 delete-translation.png160.38 KBYesCT
#75 menu-link-content-help.png90.79 KBYesCT
#75 create-translation.png262.86 KBYesCT
#75 only-language.png200.58 KBYesCT
#75 language-on-menu.png146.79 KBYesCT
#69 increment.txt13.19 KBpwolanin
#69 2301317-68.patch269.87 KBpwolanin
#61 increment.txt5.84 KBpwolanin
#61 2301317-61.patch265.41 KBpwolanin
#58 increment.txt3.65 KBpwolanin
#58 2301317-58.patch265.86 KBpwolanin
#57 2301317-57.patch264.83 KBpwolanin
#57 increment.txt14.84 KBpwolanin
#50 increment.txt1.01 KBpwolanin
#50 2301317-49.patch259.23 KBpwolanin
#46 2301317-46.patch257.39 KBeffulgentsia
#42 interdiff_wim.txt730 bytesWim Leers
#42 interdiff_dawehner.txt20.08 KBWim Leers
#42 2301317-42.patch257.4 KBWim Leers
#28 xhprof runs.zip195.17 KBWim Leers
#27 interdiff.txt46.85 KBWim Leers
#27 2301317-27.patch260.39 KBWim Leers
#26 increment.txt2.01 KBpwolanin
#26 2301317-26.patch242.5 KBpwolanin
#25 2301317-25.patch281.86 KBpwolanin
#25 2301317-25-only-part4-do-not-test.patch240.49 KBpwolanin
#23 2301317-23.patch280.72 KBpwolanin
#23 increment.txt1.17 KBpwolanin
#23 2301317-23-only-part4-do-not-test.patch242.56 KBpwolanin
#22 2301317-22.patch280.44 KBpwolanin
#22 increment.txt2.06 KBpwolanin
#22 2301317-22-only-part4-do-not-test.patch241.88 KBpwolanin
#19 increment.txt1.41 KBpwolanin
#18 2301317-18.patch281.04 KBpwolanin
#17 menu-part4-rm-mli-do-not-test.patch503 byteseffulgentsia
#15 menu-recache-systemmenublock-do-not-test.patch1.57 KBeffulgentsia
#13 2301317-13.patch281 KBpwolanin
#13 increment.txt2.31 KBpwolanin
#13 2301317-13-part4-only-do-not-test.patch242.45 KBpwolanin
#7 xhprof runs.zip739.06 KBWim Leers
#7 navlinks_stupidity-do-not-test.patch1.66 KBWim Leers
#7 paramscache-do-not-test.patch2.66 KBWim Leers
#6 2301317-part4-only-do-not-test.patch242.53 KBpwolanin
#6 increment.txt11.17 KBpwolanin
#6 2301317-part3-part4-6.patch275.68 KBpwolanin
menu-2256521-conversion-combined.patch484.42 KBeffulgentsia
menu-2256521-conversion-review-do-not-test.patch242.9 KBeffulgentsia
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

YesCT’s picture

Issue tags: +Needs tests
YesCT’s picture

Issue tags: -Needs tests

#39 3, those were just added to part 1. great.

xjm’s picture

Issue summary: View changes
xjm’s picture

Priority: Normal » Critical
pwolanin’s picture

Here's the full remaining patch through step 4, the patch for step 4 on top of step 3, and the incremental change since Alex's 1st patch on the issue with some fixes and method name changes from earlier parts.

Wim Leers’s picture

In order for this part 4 patch to be committed (which is really the one that brings it all together), #1805054-125: Cache localized, access filtered, URL resolved, and rendered menu trees needs to be answered:

In #73, patch+hack was same as HEAD, now it's 10% slower. Probably due to the merging of Wim's work (better code sometimes has a perf cost). Would be good to know what specifically is responsible for that to make an informed decision on whether to accept it or try to optimize.

That's what this comment wants to do. Since it's easy to make mistakes in profiling, I included sufficient information for anyone to reproduce this.

The very short answer is: the patch now removes menu block which it didn't do previously, plus menu tree loading/building/rendering became ~10% slower (most likely due to the added abstractions, which is necessary for #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees).

… and in doing this profiling, I've uncovered a wider performance problem.


Tested with HEAD === 50ac4700e21f401fea8cca2379e111a912173a95.


For those of you who don't want to read the full analysis, I've put the summary/conclusion and takeaways at the top — but please do read everything if you're going to comment regarding the profiling, otherwise you're not acting on the full info.

Summary/conclusion

  1. From step 1: I repeated effulgentsia's test and found the overall regression to be 29%, not 25%. Most likely explained by the different environment (hardware, PHP version …) and the used metric (ab).
  2. From step 2: I benchmarked & profiled a response that doesn't render any menu, and in fact doesn't render any HTML, and I still found a 10% regression (note that this was already present before my patch was merged). The root cause is: more services are instantiated, even though they're not used! This is a wider problem in Drupal core, and will only get worse when you add contrib modules.
    A possible solution is lazy services — see #1973618-27: DIC: Lazy instantiation of service dependencies (ProxyManager for "proxy services").
  3. From step 3, optimization 1: this is the most apple-to-apple-like comparison possible. Instead of the 10% regression that effulgentsia found, I found a ~20% regression (though indeed a ~10% regression in number of function calls).
  4. From step 3, optimization 2: in HEAD, SystemMenuBlock is cached, in pwolanin's original patch this is also the case, but now that my patch is merged, this is no longer the case. HEAD is incorrectly caching menu blocks per role, hence e.g. entity access checks are not running at all, which yields incorrect results. So for this optimization, I enabled the same caching again.
    • Benchmark: 29% -> 8% regression.
    • Profiling: function calls: 2.9% regression, wall time: 8.6% regression.
  5. From the intermezzo: With the two simple optimizations from above applied, which brought us quite close already to where we need to get (no regression) considering where we came from, I roughly simulated part of the performance improvement #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees will bring (I disabled the rendering of primary and secondary menus, since they will become cached as well). This singlehandedly turns the 8% regression into a 3% improvement — for the super simple primary and secondary menus that core ships with — in the real world, the difference will be much bigger.
  6. From step 3, optimization 3: in HEAD, the typical menu tree parameters (which depends on the active trail and the links marked as "expanded") are cached per-page. When cleaning that up, pwolanin and I decided to not prematurely optimize that, so in the current patch there's no caching for it anymore. Restoring caching for that (paramscache-do-not-test.patch) is something we probably want to include as part of this path. Doing that
    • Benchmark: 29% -> 8% -> 5% regression.
    • Profiling: function calls: no regression (9 less!), wall time: 3.6% regression.
  7. From the observation: menu tree loading/transforming/rendering has become slightly slower (~10%) — probably due to the added abstractions needed for #1805054. However, there's a known inefficiency in HEAD where the main and secondary menus are rendered twice, which of course means that this slowdown is stressed excessively. #1805054 will fix that, but if we simulate that by statically caching the rendering of these menus (navlinks_stupidity-do-not-test.patch), we can get a better sense of what #1805054 will bring us. We also get a better sense how hard or simple it will be to make improvements that offset the regression caused by this patch.
    • Benchmark: 29% -> 8% -> 5% -> 2.8% regression.
    • Profiling: function calls: 1.2% improvement, wall time: 0.2% improvement.

Takeaways

Rendering menus is excessively slow
It's clear that Drupal 8 spends an inordinate amount of time on rendering menus — both before and after this patch. Roughly 20% of the total page generation time is spent on rendering menus, both before and after this patch.
Menu rendering becomes 10% slower
To allow it to be made an order of magnitude faster by making menu rendering cacheable in #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees.
Overall page generation time regression is surmountable
  1. We MUST implement something like #2289319-2: Cache getPathFromRoute() result in the {cache_menu}.tree-data cache (step 3, optimization 1), but doing that is out of scope for this issue. That will also help all other rendered links.
  2. We COULD bring back menu block caching to this patch (step 3, optimization 2), even though the results will continue to be wrong, or we could let #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees remove it.
  3. We MUST include caching of the typical menu tree parameters in this patch (step 3, optimization 3).
  4. We COULD include static caching of rendered primary/secondary menus; there's no harm in doing so, it soothes the pain this patch brings, and it would be removed by #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees.

With 2. and 3. as part of this patch and 1. added later, the effective regression is only 3.6% (this is what step 3, optimization 3 shows us).


The context you need to understand this profiling

This issue is one of 5 parts that #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 was split into. This issue represents part 4, which performs the actual conversion — the first three patches introduced the new code, without actually using it. Those patches landed after thorough code review.
For this patch to land, code review alone isn't sufficient; it also needs to prove that it introduces at most a minimal regression.

A minimal regression is acceptable because #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees promises to bring render caching of menus, which will bring significant performance improvements that would easily offset a minimal regression in this issue (see #1805054-97: Cache localized, access filtered, URL resolved, and rendered menu trees — 10 to 20% improvement of overall page load time, depending on the complexity of the page and menus).

However, to do that, I had to refactor MenuTree significantly. Unfortunately, pwolanin did the same in #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. So we had to merge our work.

Before we merged our patches, effulgentsia did profiling at #2256521-73: [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 found this patch to be 15% slower. However, a large part of that can be attributed to the very inefficient LinkGenerator. With the hack in #2289319-2: Cache getPathFromRoute() result in the {cache_menu}.tree-data cache applied, this patch was deemed to be essentially equally fast as HEAD.

After I merged my patch though, effulgentsia repeated the above (so this patch + the hack of #2289319-2) at #2256521-125: [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 found a ~10% regression (without that hack, it's 25%). My goal was to identify the root cause of that ~10% regression.

Note that since effulgentsia did all benchmarking/profiling using the anonymous user, I did so as well.


Tooling :(

Let me start by saying that I think that determining if there is a performance regression by running ab is less than great. It's easy to get wildly varying results between different runs, even when doing 100 requests. I think it's more appropriate to compare the number of CPU instructions.
There's easily a ~10% difference between different ab runs!
Sadly, XHProf doesn't give us that information either. Because even in XHProf runs, I see quite large variance.

In short: the tooling is lacking.


Step 1: high-level benchmarking

  1. Best of ten runs (ab -c1 -n100 http://domain/menu-bench, PHP 5.5.11, OpCache enabled, XDebug disabled, XHProf disabled):
    1. HEAD: 148.087 ms/request
                      min  mean[+/-sd] median   max
        Connect:        0    0   0.0      0       0
        Processing:   134  148  10.9    144     177
        Waiting:      123  136  10.1    133     164
        Total:        134  148  10.9    144     177
      
        Percentage of the requests served within a certain time (ms)
          50%    144
          66%    149
          75%    152
          80%    157
          90%    168
          95%    173
          98%    176
          99%    177
         100%    177 (longest request)
      
    2. patch (#2301317-6: MenuLinkNG part4: Conversion): 190.349 ms/request
                      min  mean[+/-sd] median   max
        Connect:        0    0   0.0      0       0
        Processing:   169  190  16.7    185     245
        Waiting:      158  178  15.9    173     229
        Total:        169  190  16.7    185     245
      
        Percentage of the requests served within a certain time (ms)
          50%    185
          66%    191
          75%    201
          80%    203
          90%    216
          95%    231
          98%    239
          99%    245
         100%    245 (longest request)
      

    This is a 29% regression.
    NOTE: one of the runs with the patch was 182.752 ms/request, but since this is was not reproducible, I omitted that.

  2. Best of ten runs (ab -c1 -n100 http://domain/menu-bench, PHP 5.5.11, OpCache enabled, XDebug disabled, XHProf disabled, APCu enabled, APC class loader enabled):
    1. HEAD: 144.185 ms/request
                      min  mean[+/-sd] median   max
        Connect:        0    0   0.0      0       0
        Processing:   131  144  12.9    139     197
        Waiting:      120  133  12.1    128     184
        Total:        131  144  12.9    139     197
      
        Percentage of the requests served within a certain time (ms)
          50%    139
          66%    142
          75%    149
          80%    157
          90%    165
          95%    172
          98%    178
          99%    197
         100%    197 (longest request)
      
    2. patch (#2301317-6: MenuLinkNG part4: Conversion): 185.465 ms/request
                      min  mean[+/-sd] median   max
        Connect:        0    0   0.0      0       0
        Processing:   163  185  18.2    178     241
        Waiting:      151  173  17.5    166     226
        Total:        163  185  18.2    178     241
      
        Percentage of the requests served within a certain time (ms)
          50%    178
          66%    190
          75%    195
          80%    203
          90%    215
          95%    223
          98%    238
          99%    241
         100%    241 (longest request)
      

    This is a 29% regression as well. Since it yields the same result, I'll continue testing with this environment: everything below is tested with APCu enabled and the APC class loader enabled.

Step 2: profiling a response that does not do anything menu-related

However, let's first make sure this is not because part 4 has not yet removed dead code. In other words: let's take a look at the same benchmark (100 requests, single concurrency, fastest out of 3) when we request /system/ajax:

  1. HEAD: 41.823 ms/request
                    min  mean[+/-sd] median   max
      Connect:        0    0   0.0      0       0
      Processing:    37   42   3.7     41      54
      Waiting:       33   37   3.4     37      48
      Total:         37   42   3.7     41      54
    
      Percentage of the requests served within a certain time (ms)
        50%     41
        66%     42
        75%     44
        80%     45
        90%     47
        95%     49
        98%     53
        99%     54
       100%     54 (longest request)
    
  2. patch (#2301317-6: MenuLinkNG part4: Conversion): 46.060 ms/request
                    min  mean[+/-sd] median   max
      Connect:        0    0   0.0      0       0
      Processing:    39   46   5.3     45      64
      Waiting:       34   41   4.7     40      57
      Total:         39   46   5.3     45      65
    
      Percentage of the requests served within a certain time (ms)
        50%     45
        66%     48
        75%     49
        80%     50
        90%     53
        95%     57
        98%     63
        99%     65
       100%     65 (longest request)
    

This is a 10% regression. In absolute numbers, it's small, but it's consistently reproducible.

While this of course does not represent the full regression we saw above, nor is it a representative test, it is indicative of a problem that lies somewhere else than menu rendering; somewhere lower. Since the system.ajax route does not even trigger HTML page rendering, I can't help but wonder how this is possible. Can this be attributed to the increased number of services?

So I did some profiling, doing 10 runs of each (HEAD & patch) and selecting the fastest run. Looking at the profiling results, I keep seeing the class loader and DIC. So I suspect that the increase in function calls and corresponding average increase in page generation time can be attributed completely to the increase in number of services.

system.ajax on HEAD system.ajax with patch Diff Diff%
Number of Function Calls 9,208 9,326 118 1.3%
Incl. Wall Time (microsec) 53,493 56,258 2,765 5.2%
Incl. MemUse (bytes) 6,497,560 6,666,568 169,008 2.6%
Incl. PeakMemUse (bytes) 6,549,656 6,718,336 168,680 2.6%

In other words: everything indicates that this shift in the page generation time (in the wrong direction) is primarily caused by additional I/O and secondarily by additional function calls, both of which are direct consequences of the increase in the number of services.

In any case, this is pretty much a dead end for profiling the menu link homestretch patches' impact; but it's useful to keep track of, because it does explain a small part of the regression. It explains several milliseconds.

Further investigation

I investigated further and the good news that I've now got the full explanation. The bad news is that it's probably not easy to fix.

  1. Just adding services does not have a performance impact.
  2. Parameter conversion services are always loaded, hence this new service does have an impact:
    paramconverter.menu_link:
      class: Drupal\Core\ParamConverter\MenuLinkPluginConverter
      tags:
        - { name: paramconverter }
      arguments: ['@plugin.manager.menu.link']
    
  3. Event subscriber services also are always loaded, hence this service does now have an impact due to its added dependency (this used to call procedural code, which is why it wasn't a problem before) on a newly added service:
      router.rebuild_subscriber:
        class: Drupal\Core\EventSubscriber\RoutebuildSubscriber
        arguments: ['@router.builder', '@lock', '@plugin.manager.menu.link']
        tags:
          - { name: event_subscriber }
    

    To make matters worse, @plugin.manager.menu.link in turn depends on two newly added services: @menu.tree_storage (which in turn depends on the newly added @cache.menu) and @menu_link.static.overrides.

Together, points 2 and 3 explain the ~4ms regression that we saw in the benchmarks. If I comment them, the number of function calls with the patch drops below that of HEAD.

To solve this out-of-scope problem, I think Symfony's lazy services may be a good option — see #1973618-27: DIC: Lazy instantiation of service dependencies (ProxyManager for "proxy services").

Step 3: profiling /menu-bench

Not having gotten useful clues from step 2, I started doing general profiling of the /menu-bench response. I did 10 runs of each again and selected the fastest run.

Run #menubench-head Run #menubench-patch Diff Diff%
Number of Function Calls 63,707 76,782 13,075 20.5%
Incl. Wall Time (microsec) 203,570 256,011 52,441 25.8%
Incl. MemUse (bytes) 16,617,216 17,965,816 1,348,600 8.1%
Incl. PeakMemUse (bytes) 16,671,360 18,002,864 1,331,504 8.0%

Now, the difference has become more pronounced: I'm seeing a ~20% increase in function calls and a ~25% increase in page generation time. So it's indeed worse than what was reported by effulgentsia at #2256521-73: [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 (15%).

This is not news though, effulgentsia already said this at #2256521-125: [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: there was a ~25% increase in page generation time in his tests as well (but, sadly, he only measured using ab, which as I've explained above, can result in highly varying numbers — I'm finding 29% in this report, but only because I tried to find the fastest possible run — if I didn't do that, it could just as well have been 25% or 20%).

Optimization 1: effulgentsia's patch to compensate for expensive link generation

However, at #2256521-73, by applying #2289319-2: Cache getPathFromRoute() result in the {cache_menu}.tree-data cache, he gained parity, and at #2256521-125, by applying that same patch, he found a 10% regression. Let's see if I can reproduce that as well:

Run #menubench-head Run #menubench-patch_patch Diff Diff%
Number of Function Calls 63,707 69,091 5,384 8.5%
Incl. Wall Time (microsec) 203,570 242,716 39,146 19.2%
Incl. MemUse (bytes) 16,617,216 17,804,488 1,187,272 7.1%
Incl. PeakMemUse (bytes) 16,671,360 17,839,896 1,168,536 7.0%

As you can see, I can't quite reproduce this. There's a ~10% (8.5%) increase in function calls, but there's a ~20% (19.2%) increase in page generation time — significantly more than the 10% he reported. But, again, that might be because that was measured using ab.

(I didn't test with ab here because normally, ab & XHProf results should at least be in the same ballpark, which is not the case here. So I didn't bother.)

Optimization 2: menu block caching

One of the things I contributed to this patch, is that menu blocks (SystemMenuBlock) are no longer cached (because right now, in HEAD, it's incorrectly caching menu blocks per role, hence e.g. entity access checks are not running at all, which yields incorrect results). That correction is definitely going to contribute to that ~20% regression in page generation time. So if I manually enable caching for the Tools and Footer menu blocks and test again, I should see a sharp difference — both in profiling and in benchmarking.

Run #menubench-head Run #menubench-patch_patch_blockcache Diff Diff%
Number of Function Calls 63,707 65,555 1,848 2.9%
Incl. Wall Time (microsec) 203,570 221,115 17,545 8.6%
Incl. MemUse (bytes) 16,617,216 15,795,312 -821,904 -4.9%
Incl. PeakMemUse (bytes) 16,671,360 15,848,944 -822,416 -4.9%

Best of ten runs (ab -c1 -n100 http://domain/menu-bench, PHP 5.5.11, OpCache enabled, XDebug disabled, XHProf disabled, APCu enabled, APC class loader enabled):

  1. HEAD: 144.185 ms/request (from above)
  2. patch (#2301317-6: MenuLinkNG part4: Conversion) + #2289319-2: Cache getPathFromRoute() result in the {cache_menu}.tree-data cache + cached menu blocks: 155.142 ms/request (down from 185.465 ms/request, a 20% reduction)
      Time per request:       155.142 [ms] (mean)
      Time per request:       155.142 [ms] (mean, across all concurrent requests)
      Transfer rate:          119.70 [Kbytes/sec] received
    
      Connection Times (ms)
                    min  mean[+/-sd] median   max
      Connect:        0    0   0.0      0       0
      Processing:   135  155  15.1    148     199
      Waiting:      124  144  14.3    138     185
      Total:        135  155  15.1    148     199
    
      Percentage of the requests served within a certain time (ms)
        50%    148
        66%    156
        75%    168
        80%    170
        90%    183
        95%    187
        98%    194
        99%    199
       100%    199 (longest request)
    

So, benchmarking-wise, instead of a 29% regression, we're now looking at an ~8% (7.6%) regression. And profiling-wise it's only a 2.9% increase in function calls and 8.6% increase in wall time.

Now we can finally look at the interesting/hard part of figuring out why this is the case.

Important intermezzo

But before doing so, I'd like to call out that the extra cost that I added, to add the abstractions necessary to make #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees possible, will make it so that 1) the primary and secondary menus will be cached as well (they're not blocks currently), 2) instead of HEAD's behavior, which is for menu rendering time to scale linearly with the number of menu links, we will get (amortized) constant rendering time. If I simulate this roughly (by commenting the rendering of the primary and secondary menus in theme.inc and common.inc), then I get a benchmark result of 139.749 ms/request, which is 3% faster than HEAD.
(If that 3% improvement disappoints you: remember, that 3% will only become bigger as you end up with more complex menus — I turned a 8% regression into a 3% improvement with a primary menu that only contains the "Home" menu link and a secondary menu that only contains "My account" and "Log out" links! If anything, this quite devastatingly shows how slow menu rendering is, even when none of your menu links needs entity access checks.)

Optimization 3: Building the typical menu tree parameters is no longer cached

It was deemed a premature optimization to cache the results of MenuLinkTree::getCurrentRouteMenuTreeParameters() (which MenuTree::buildPageData() does in HEAD), but it seems this might be worth it after all.

Having applied the attached paramscache-do-not-test.patch, this is what we get…

Best of ten runs (ab -c1 -n100 http://domain/menu-bench, PHP 5.5.11, OpCache enabled, XDebug disabled, XHProf disabled, APCu enabled, APC class loader enabled):

  1. HEAD: 144.185 ms/request (from above)
  2. patch (#2301317-6: MenuLinkNG part4: Conversion) + #2289319-2: Cache getPathFromRoute() result in the {cache_menu}.tree-data cache + cached menu blocks + paramscache-do-not-test.patch: 151.198 ms/request (down from 185.465 ms/request, a 23% reduction)
                    min  mean[+/-sd] median   max
      Connect:        0    0   0.0      0       0
      Processing:   132  151  16.8    144     209
      Waiting:      122  140  15.7    134     197
      Total:        132  151  16.8    144     209
    
      Percentage of the requests served within a certain time (ms)
        50%    144
        66%    150
        75%    155
        80%    165
        90%    181
        95%    188
        98%    202
        99%    209
       100%    209 (longest request)
    

So, benchmarking-wise, instead of a 29% regression, we're now looking at an ~5% regression. And profiling-wise we now have a very slight decrease in the number of function calls (-9 calls), and only a 3.6% increase is left in wall time!

Run #menubench-head Run #menubench-patch_patch_blockcache_paramscache Diff Diff%
Number of Function Calls 63,707 63,698 -9 -0.0%
Incl. Wall Time (microsec) 203,570 210,870 7,300 3.6%
Incl. MemUse (bytes) 16,617,216 15,785,152 -832,064 -5.0%
Incl. PeakMemUse (bytes) 16,671,360 15,837,232 -834,128 -5.0%

Observation: menu tree loading/transforming/rendering has become slightly slower

In HEAD, the inclusive wall time of MenuTree::renderMenu() encompasses the parameter building and loading/transform/rendering. The profiled result took 41.4 ms.

With the patch and the above 3 optimization, the inclusive wall time of MenuLinkTree::getCurrentRouteMenuTreeParameters() plus MenuLinkTree::(load|transform|build)() took 45.1 ms.

So this became about 10% slower — quite likely due to the increased abstraction, which we did both for maintainability as well as to allow for smarter caching to be added in #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees.

Since we made the overall rendering of menus 10% slower, any double rendering of menus is going to affect us twice as much.

And unfortunately, we *do* have double rendering of menus: the incredibly silly/awful hoops we have to jump through to get cache tags for the primary and secondary menus set in HEAD require us to render those menus twice. #1805054 already fixes that, but if we "fix" that in an ugly way for now, to better approximate what #1805054 will bring us, by introducing static caching in menu_navigation_links() to essentially statically cache the rendered primary/secondary menus (navlinks_stupidity-do-not-test.patch), then this is what we get…

Best of ten runs (ab -c1 -n100 http://domain/menu-bench, PHP 5.5.11, OpCache enabled, XDebug disabled, XHProf disabled, APCu enabled, APC class loader enabled):

  1. HEAD: 144.185 ms/request (from above)
  2. patch (#2301317-6: MenuLinkNG part4: Conversion) + #2289319-2: Cache getPathFromRoute() result in the {cache_menu}.tree-data cache + cached menu blocks + paramscache-do-not-test.patch + navlinks_stupidity-do-not-test.patch: 148.238 ms/request (down from 185.465 ms/request, a 25% reduction)
                    min  mean[+/-sd] median   max
      Connect:        0    0   0.0      0       0
      Processing:   133  148  11.0    145     182
      Waiting:      123  137  10.2    133     168
      Total:        133  148  11.0    145     182
    
      Percentage of the requests served within a certain time (ms)
        50%    145
        66%    149
        75%    155
        80%    157
        90%    165
        95%    171
        98%    180
        99%    182
       100%    182 (longest request)
    

So, benchmarking-wise, instead of a 29% regression, we're now looking at a 2.8% regression. And profiling-wise we now have decreases in both function calls (-1.2%) and wall time (-0.2%)!

Run #menubench-head Run #menubench-patch_patch_blockcache_paramscache_navlinks Diff Diff%
Number of Function Calls 63,707 62,952 -755 -1.2%
Incl. Wall Time (microsec) 203,570 203,123 -447 -0.2%
Incl. MemUse (bytes) 16,617,216 15,790,456 -826,760 -5.0%
Incl. PeakMemUse (bytes) 16,671,360 15,840,336 -831,024 -5.0%

Combined with the observed improvement in Important intermezzo section above, I think it's obvious that it will indeed be quite easy to compensate for the minor regression introduced here.

pwolanin’s picture

I'm having trouble parsing this in full, but when we have render caching we can remove the use of the @cache.menu service.

We should also figure out how to lazy fetch services like @menu_link.static.overrides this is only used on rebuild, so we don't need the instance on 99.9% of page loads, so let's give attention to #1973618: DIC: Lazy instantiation of service dependencies (ProxyManager for "proxy services") soon.

I thinnk the overall message is that we need to move forward so we can achieve render caching?

Wim Leers’s picture

The actionable steps are under Overall page generation time regression is surmountable.

But yes, we can move forward, but we must include the caching of tree parameters (see paramscache-do-not-test.patch for a rough version). And we could choose to re-enable caching of menu blocks to have better performance until #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees lands.

tstoeckler’s picture

I have a hard time following @WimLeers' comment as I cannot find the referenced benchmark by @effulgentsia and, thus, do not know what /menu-bench does. Could someone help? Thanks! :-)

Wim Leers’s picture

#10: Sorry — /menu-bench is provided by the benchmarking module that effulgentsia provided over at #2256521-125: [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.

(I wrote so much, yet still failed at making every detail clear :()

Wim Leers’s picture

Part of the problems I found in "step 2" in my profiling above can apparently be solved by splitting RouterRebuildSubscriber into two parts (thanks to dawehner & catch in IRC!).

Apparently, event subscribers are only instantiated if the event they're subscribing to is actually fired. But because the onKernelTerminate event happens on every request, RouterRebuildSubscriber is always instantiated, plus its dependencies, which this series of patches has made more expensive by adding MenuLinkManager (which in turn depends on another cascade of services).

However, MenuLinkManager is only needed for the onRouterRebuild event. So we could split that off into a separate event subscriber, which means that big cascade of services will only need to happen when it's actually necessary — et voilà, part of the performance problem would be fixed :)

pwolanin’s picture

Here's a patch with recent changes from part3 and core

Status: Needs review » Needs work

The last submitted patch, 13: 2301317-13.patch, failed testing.

effulgentsia’s picture

Thanks, Wim, for the awesome analysis in #7.

in HEAD, SystemMenuBlock is cached, in pwolanin's original patch this is also the case, but now that my patch is merged, this is no longer the case. HEAD is incorrectly caching menu blocks per role, hence e.g. entity access checks are not running at all, which yields incorrect results. So for this optimization, I enabled the same caching again.
Benchmark: 29% -> 8% regression

I'll post my numbers shortly. I also found a large difference when caching SystemMenuBlock, though not as large. I don't see why it should be this issue's scope to change the caching of SystemMenuBlock. Yes, HEAD is wrong in that it doesn't take into account per-user access checks, but I think we should deal with that in a separate issue, so that we can isolate the performance impact of that from the rest of this patch. Here's the patch for reverting that change from this issue.

effulgentsia’s picture

Ok, here's a comparison of my results (using ab) vs. Wim's (inclusive wall time from xhprof) for the 3 optimizations he explains in #7:

Patch as-is (#13):  25.8% (Wim) 25.5% (me)
Optimization 1:     19.2% (Wim)  9.8% (me)
Optimization 1+2:    8.6% (Wim)  1.3% (me)
Optimization 1+2+3:  3.6% (Wim) -0.7% (me)

As a refresher, optimization 1 is #2289319-2: Cache getPathFromRoute() result in the {cache_menu}.tree-data cache, optimization 2 is the patch in #15, and optimization 3 is the "paramscache-do-not-test.patch" in #7.

Some notes, caveats, opinions:

  1. Since #13 failed tests, my numbers might be bogus if something about the page was rendered incorrectly.
  2. Wim states in #7 that ab is a poor tool due to high variance between runs. On my machine, I get very stable numbers with ab: much more stable than I get with xhprof. I have had machines in the past (e.g., my previous MBP which was a late 2010 model) where the CPU fan would kick on all the time, and cause lots of ab variance, but my current MBP (late 2013) does not have that problem.
  3. The biggest difference between mine and Wim's numbers is the benefit of optimization 1. That could use some more study. Perhaps someone else could try? The scenario is testing the /menu-bench URL as an anonymous user on a fresh Standard profile install after enabling the "Menu Bench" module provided in #2256521-125: [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 priming caches by visiting /menu-bench as anon at least once prior to benchmarking/profiling. Remember to always do a re-install whenever applying any of the optimization patches (not all of them strictly need it, but it's safer that way).
  4. My /menu-bench scenario is tailored to test a menu with a lot of MenuLinkContent (i.e., custom / content-entity-backed) links. The scenario isn't a good representation of a real site's menu: all the links go to the same route, that route's access requirement is set to TRUE (i.e., any real access checking is skipped), and the menu hierarchy is flat (100 top-level links). I chose this scenario because what I was most concerned about was regressions in the patch related to general link iterating/rendering, and any overhead from the custom links as content entities architecture (since we know there's overhead with content entities). The patch has already been optimized to not load the content entities at all at runtime (at least for a monolingual site), which is why with a few of the other optimization listed above, we're able to reach near parity.
  5. I think what Wim found in #7 with respect to 4ms or so of overhead even on routes without any menu rendering (e.g., system/ajax) is a great find, and I leave it to catch to decide whether something like #12 should be a blocker for this issue or a followup.
  6. Potentially, with all of the possible optimizations uncovered (not just the three mentioned at the top of this comment), if we did all of them (whether here or in followups), the combination could result in a performance improvement of the menu system relative to HEAD, even ignoring what we end up achieving with render caching in #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees. That would be awesome, since on some sites with very dynamic menu needs, the render cache hit ratios might not be very good. It also makes sense that such a result is possible, since the menu link plugin architecture benefits from a dedicated materialized table for runtime querying rather than HEAD's implementation of menu links as entities (not content or config entities, but still entities, and therefore, entity system overhead). What's been in the way of us seeing those gains is losses elsewhere, so it's great that Wim found where some of those are.
effulgentsia’s picture

Oh, can we also add this to this issue (ahead of the full deletion in part5)? Because in HEAD, it's a required module, it actually results in a WSOD when you install the Menu Bench module, so my tests in #16 were with this also applied.

pwolanin’s picture

Status: Needs work » Needs review
Issue tags: +MenuSystemRevamp
FileSize
281.04 KB

some small form fixes and file renames

pwolanin’s picture

FileSize
1.41 KB

here's the increment

Status: Needs review » Needs work

The last submitted patch, 18: 2301317-18.patch, failed testing.

pwolanin queued 18: 2301317-18.patch for re-testing.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
241.88 KB
2.06 KB
280.44 KB

Silly testbot - had a sporadic fail on BrokenSetUpTest because it ran out of memory or something. retest passed.

Here's a patch incorporating the 2 changes from @effulgentsia, both as full patch and only part4

pwolanin’s picture

New set of patches will a small change to menu_ui.admin.inc that was incorrectly in part3 instead of here.

Status: Needs review » Needs work

The last submitted patch, 23: 2301317-23.patch, failed testing.

pwolanin’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
240.49 KB
281.86 KB

re-rolled against 8.x, and moved one new class to part 3.

pwolanin’s picture

Issue summary: View changes
FileSize
242.5 KB
2.01 KB

And, here's the part4 patch now that part3 is committed, with some smalls cleanups since the last one from me and YesCT.

Wim Leers’s picture

FileSize
260.39 KB
46.85 KB

dawehner, pwolanin and I pushed a bunch more commits. Mostly clean-up, but also to apply some of the conclusions from the profiling. We:

  1. restored caching of the SystemMenuBlock
  2. added caching to MenuLinkTree::getCurrentRouteMenuTreeParameters()
  3. split up RouterRebuildSubscriber into two parts, to remove the added overhead on requests that don't even touch menus

#16.2: interesting! My computer definitely is overdue for replacement; I'm waiting for my computer a lot these days.
#16.3: I agree, it's concerning that our numbers are so vastly different. A third person should do profiling to hopefully figure out what's going on. Note that I'm on PHP 5.5, with OpCache, APCu and the APC class loader. Perhaps a difference in PHP execution environment helps explain this discrepancy?
#16.4: So… are you suggesting we do profiling with different situations as well? In the profiling I did at #1805054-97: Cache localized, access filtered, URL resolved, and rendered menu trees, I compared 6 different situations, which I think might be more representative of typical Drupal sites.
#16.5: That's already fixed now :)
#16.6: (What I'm about to say is slightly off-topic, but at the same time related to this series of patches, and something we must keep in mind.) I think that — roughly speaking — it's unfair to expect Drupal 8 core to be optimized for sites that have "very dynamic menu needs". Because that implies lower cache hit ratios. It's impossible to not have lower cache hit ratios. However, if they have such needs, then it's actually quite easy to fix their performance problems: change the configuration of the optimized access checking tree manipulator of #1805054 to not only cache per role, but cache per role *and* group *and* region (or whatever the actual dimensions are by which they need to cache). They'd only need to ensure they have sufficient room for additional cache entries. And yes, the individual cache entries would each be used less often, but it would mean that again fewer access checkers need to run on each page load, since access checkers that vary by group or region (in my example) will also be cached. In essence, this is horizontal scaling, where ultimately you could end up at caching per user… which is what the most complex sites out there (like Facebook) do.


So, time for another round of profiling (superficial this time), to see where we're currently at. Again applying the out-of-scope optimization (#2289319-2: Cache getPathFromRoute() result in the {cache_menu}.tree-data cache) to get an apples-to-apples comparison. Tested with ef4e932d716742952a72a5ef23d538f65fcdbf78. And the same environment as in #7 (with the APC class loader).

Benchmarking

Fastest out of ten times ab -c 1 -n100:

  1. HEAD: 133.066 ms/request (versus 144.185 in #7, so HEAD became 8.4% faster since last time I tested — wow! Do we know which patch did that?)
  2. HEAD + #2289319-2: Cache getPathFromRoute() result in the {cache_menu}.tree-data cache + attached patch: 128.675 ms/request

Or… a 3.5% improvement!

Unfortunately, there's one big caveat: one run with the patch was 128 ms, another was 129, but most were 134 ms and a few even 139/140. For HEAD, it was *always* 133, 134 or 135 on average.
Together with #16.2, I suspect part of the variance is due to my computer's hardware being weak/fragile (I did these tests with only Chrome, Terminal and Finder open, plus of course my web server stack — and still such variance!).
But in any case, we're roughly "on-par" performance-wise! It could be a consistent improvement, if my computer wasn't so stupid.

Profiling

Here we can see again that HEAD improved its memory consumption significantly: from 16.6 MB to 14.1 MB. But that improvement was made somewhere else than in the menu system, hence the 5% improvement we saw in #7 has now become a 7% memory improvement. Instead of 9 function calls less, we're now at 81 function calls less, so it's a 0.1% improvement now. But sadly, we still have a 5% regression in inclusive wall time… though — again — this could be attributed to my computer being too slow/overtaxed.

#menubench-head #menubench-patch_patch Diff Diff%
Number of Function Calls 63,351 63,270 -81 -0.1%
Incl. Wall Time (microsec) 193,351 202,647 9,296 4.8%
Incl. MemUse (bytes) 14,176,648 13,197,472 -979,176 -6.9%
Incl. PeakMemUse (bytes) 14,213,648 13,232,936 -980,712 -6.9%
Wim Leers’s picture

FileSize
195.17 KB

I forgot to attach the corresponding XHProf runs.

Wim Leers’s picture

Note that in #7, we saw a ~0.8 MB memory improvement. But in #27, we see a ~1 MB memory improvement. I didn't verify this, but this is probably thanks to dawehner having split up RouterRebuildSubscriber :)

xjm’s picture

So, I know some work has been done on change records already, but I see no change records referenced in the sidebar of this issue or #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 need:

  • A list of all the existing change records updates that will be needed for this patch. Each such change record should be edited to add a reference to this issue, and we should identify what will need to be changed. If it's small, we can do it after the patch, but if it's large, we should create a new draft with the new text.
  • A change record specifically for the new changes, also referencing this issue.

Also, can we please get a more complete summary of what is changed in this patch, etc.? There were also several points of feedback pushed onto this issue from previous ones and I don't know if they've been addressed yet or not.

pwolanin’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Linked the change record draft to ALL 7 issues we've been working on. It includes a list of past change records that needs to be updated, but is otherwise a bit of a stub still, and the real details should go in the handbook.

I don't think we postponed any earlier concerns except the performance fixes (done) and possibly only #2304363: Make MenuLinkTreeElement's properties protected, add getters, add limited setters, though I'd prefer to keep that as a follow-up

pwolanin’s picture

Issue summary: View changes
pwolanin’s picture

some doc updates needed - these are still referencing removed functions

core/modules/menu_link/src/MenuTreeInterface.php
24:   *   A data structure representing the tree as returned from menu_tree_data.

core/modules/menu_ui/src/MenuForm.php
252:   *   The menu_tree retrieved by menu_tree_data.
xjm’s picture

Assigned: Unassigned » xjm
Issue summary: View changes
Issue tags: +Needs followup

Thanks @pwolanin, that helps a ton.

It looks like the change record draft could use a lot of work. It previously included a list of some of the change records that need to be updated... I've transferred that here:

Here's a list of change notices that need to be updated

https://www.drupal.org/node/2240003
removed menu_get_active_trail() , menu_set_active_trail() ,menu_set_active_item($path)

https://www.drupal.org/node/2027579
The language of menus and menu links can now be configured from the user interface

https://www.drupal.org/node/1914008 Menu links have been converted to entities
D7 menu_link_load($mlid); D8 $menu_link = entity_load('menu_link', $mlid);)

https://www.drupal.org/node/1914008 Menu links have been converted to entities
hook_menu_link_alter() has been replaced by hook_menu_link_presave()

Those should reference this issue as well, since this is the issue that actually makes the changes. I'm not sure whether the list is complete... there are a lot that we should glance at and either rule out or also add:
https://www.drupal.org/list-changes/published/drupal?keywords_descriptio...

It looks like there are still a couple unresolved/unfiled @todo in the patch (at least one is a moved line of code, but others are not). I added a contributor task for checking over those as well.

Meanwhile, starting a line-by-line review.

dawehner’s picture

effulgentsia’s picture

Here's my review of everything excluding menu_ui, system, and views module changes. I'll do each of those in separate comments.

All of these are just nits. I couldn't find anything significant to complain about :)

  1. +++ b/core/includes/menu.inc
    @@ -264,18 +264,56 @@
    + * - Pass the menu tree to \Drupal\menu_link\MenuTree::build(), this will build
    

    Referencing the wrong class and namespace.

  2. +++ b/core/includes/menu.inc
    @@ -264,18 +264,56 @@
    + * $menu_tree = \Drupal::service('menu.link_tree');
    

    \Drupal::menuTree()

  3. +++ b/core/includes/menu.inc
    @@ -264,18 +264,56 @@
    + * $tree = $menu_tree->transform($tree, $manipulators);
    

    Difference between $tree and $menu_tree not clear from the variable names. Possibly a followup to improve local variable names?

  4. +++ b/core/lib/Drupal/Core/Menu/MenuLinkTree.php
    @@ -57,31 +73,56 @@ class MenuLinkTree implements MenuLinkTreeInterface {
    +    static $cached_parameters = array();
    

    Is there a reason for this to be a static local variable rather than a protected instance variable?

  5. +++ b/core/modules/entity/src/Controller/EntityDisplayModeController.php
    @@ -26,7 +27,7 @@ public function viewModeTypeSelection() {
    -          'link_path' => 'admin/structure/display-modes/view/add/' . $entity_type_id,
    +          'url' => new Url('entity.view_mode_add_type', array('entity_type_id' => $entity_type_id)),
               'localized_options' => array(),
    

    Here and elsewhere: when we make this change, do we still need 'localized_options', or should that be removed?

  6. +++ b/core/modules/menu_ui/menu_ui.admin.inc
    @@ -27,27 +32,28 @@ function theme_menu_overview_form($variables) {
           $row[] = drupal_render($indent) . drupal_render($element['title']);
    -      $row[] = array('data' => drupal_render($element['hidden']), 'class' => array('checkbox', 'menu-enabled'));
    -      $row[] = drupal_render($element['weight']) . drupal_render($element['plid']) . drupal_render($element['mlid']);
    +      $row[] = array('data' => drupal_render($element['enabled']), 'class' => array('checkbox', 'menu-enabled'));
    +      $row[] = SafeMarkup::set(drupal_render($element['weight']) . drupal_render($element['parent']) . drupal_render($element['id']));
           $row[] = drupal_render($element['operations']);
    

    Why is SafeMarkup::set() added to one of these and not the others?

  7. +++ b/core/modules/menu_ui/menu_ui.info.yml
    @@ -6,4 +6,4 @@ version: VERSION
    -  - menu_link
    +  - menu_link_content
    \ No newline at end of file
    

    The change is good, but no need to remove the trailing newline.

  8. +++ b/core/modules/toolbar/toolbar.module
    @@ -403,7 +381,19 @@ function toolbar_toolbar() {
    +      'toolbar_administration' => array(
    +        '#pre_render' => array(
    +          'toolbar_prerender_toolbar_administration_tray',
    

    This code is already inside a #pre_render. Why are we adding another pre_render layer rather than calling that function directly?

  9. +++ b/core/modules/toolbar/toolbar.module
    @@ -411,90 +401,101 @@ function toolbar_toolbar() {
    +    // @todo Change to use the plugin ID as class as titles might change.
    

    Let's get a followup issue filed for this and link to it from here.

  10. +++ b/core/modules/user/src/Tests/UserAccountLinksTests.php
    @@ -43,14 +44,14 @@ function testSecondaryMenu() {
    -      ':menu_class' => 'links',
    +      ':menu_class' => 'menu',
    

    Doesn't look like this patch has any CSS changes included. Does this change require any?

effulgentsia’s picture

For the Views module portion:

  1. +++ b/core/modules/views/src/Plugin/Derivative/ViewsMenuLink.php
    @@ -0,0 +1,52 @@
    + * Provides menu links for views.
    

    Should "views" be capitalized?

  2. +++ b/core/modules/views/src/Plugin/Derivative/ViewsMenuLink.php
    @@ -0,0 +1,52 @@
    +    // @todo Decide what to do with all the crazy logic in views_menu_alter() in
    +    // https://drupal.org/node/2107533.
    

    Is this @todo still relevant? Looks like the linked issue is fixed, and views_menu_alter() no longer exists.

  3. +++ b/core/modules/views/src/Plugin/Menu/Form/ViewsMenuLinkForm.php
    @@ -0,0 +1,63 @@
    + * Provides an edit form for the views.
    

    Provides a form to edit Views menu links.

  4. +++ b/core/modules/views/src/Plugin/Menu/Form/ViewsMenuLinkForm.php
    @@ -0,0 +1,63 @@
    + * This provides the feature to change the title, in contrast to the static
    + * menu link form.
    

    s/change/edit/.

    Also, we should add editing of description to both the functionality and then to this doc.

  5. +++ b/core/modules/views/src/Plugin/Menu/Form/ViewsMenuLinkForm.php
    @@ -0,0 +1,63 @@
    +      // @todo how do we ensure that view is not loaded with a translation?
    +      '#default_value' => $this->menuLink->getTitle(),
    

    If the fix for this is easy, let's add it to this patch. Otherwise, let's open a followup and add a link to the @todo.

  6. +++ b/core/modules/views/src/Plugin/Menu/Form/ViewsMenuLinkForm.php
    @@ -0,0 +1,63 @@
    +    if ($this->menuLink instanceof ViewsMenuLink) {
    

    Why do we need this check? If the plugin specified to use this form, can't we assume it implements the needed interface?

  7. +++ b/core/modules/views/src/Plugin/Menu/Form/ViewsMenuLinkForm.php
    @@ -0,0 +1,63 @@
    +      $form['info']['#title'] = $message;
    

    How about making this $form['path']['#description'] and unsetting $form['info']?

  8. +++ b/core/modules/views/src/Plugin/Menu/ViewsMenuLink.php
    @@ -0,0 +1,173 @@
    +      // Just save the title to the original view.
    +      // @todo What do we do about the other properties, like weight,
    +      //   description and menu name.
    

    Any reason not to add them right now to this patch? If some of them are difficult, let's open a followup.

  9. +++ b/core/modules/views/src/Plugin/Menu/ViewsMenuLink.php
    @@ -0,0 +1,173 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function getBaseId() {
    +    $plugin_id = $this->getPluginId();
    +    if (strpos($plugin_id, 'views.') === 0) {
    +      $plugin_id = 'views';
    +    }
    +    return $plugin_id;
    +  }
    +
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function getDerivativeId() {
    +    $plugin_id = $this->getPluginId();
    +    $derivative_id = NULL;
    +    if (strpos($plugin_id, 'views.') === 0) {
    +      list(, $derivative_id) = explode('views.', $plugin_id, 2);
    +    }
    +    return $derivative_id;
    +  }
    

    Is there any code calling these? The implementation seems wrong to me. Since the plugin IDs are are all 'views_view:...', those if statements are never true.

  10. +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
    @@ -2121,15 +2121,14 @@ public function renderMoreLink() {
    -   * @param array $existing_links
    -   *   An array of already existing menu items provided by drupal.
    +   * @internal param array $existing_links An array of already existing menu items provided by drupal.*   An array of already existing menu items provided by drupal.
    

    PHPStorm messing with newlines and duplicating comments? Also, the param is removed from the signature, so maybe the whole doc for it should be as well?

  11. +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
    @@ -2121,15 +2121,14 @@ public function renderMoreLink() {
        * @see hook_menu_link_defaults()
    

    This hook got renamed. Not sure we need an @see here at all, but if we want it, perhaps it should point to the deriver?

  12. +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
    @@ -2121,15 +2121,14 @@ public function renderMoreLink() {
    -  public function executeHookMenuLinkDefaults(array &$existing_links) {
    +  public function executeHookMenuLinks() {
    

    If we're renaming this method, perhaps we should rename it to something better, like "getMenuLinks()"?

  13. +++ b/core/modules/views/src/Plugin/views/display/PathPluginBase.php
    @@ -278,8 +278,9 @@ public function alterRoutes(RouteCollection $collection) {
        * {@inheritdoc}
    +   * @return array
    

    Since we have the {@inheritdoc}, we don't need the @return.

effulgentsia’s picture

For the System module portion:

  1. +++ b/core/modules/system/src/Controller/SystemController.php
    @@ -87,66 +99,49 @@ public static function create(ContainerInterface $container) {
    +   *   The ID of and administrative path link for which to display child links.
    

    s/and/the/?

  2. +++ b/core/modules/system/src/Form/ModulesListForm.php
    @@ -262,7 +276,7 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
    -              'title' => $item['description'],
    +              'title' => $title,
    

    This is a UX change/regression. For example, for the Block module, the Configure link now says "Block layout" instead of "Configure what block content appears in your site's sidebars and other regions.". I get the desire to decouple Configure links from menu links, but I wonder if this at least needs a followup issue to discuss further.

  3. +++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
    @@ -66,8 +78,15 @@ public static function create(ContainerInterface $container, array $configuratio
    +    $manipulators = array(
    +      array('callable' => 'menu.default_tree_manipulators:checkAccess'),
    +      array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
    +    );
    +    $tree = $this->menuTree->transform($tree, $manipulators);
    

    At least in this patch, this seems to be a very common value for $manipulators. How about a followup to discuss making that a default for the transform() function in some way?

  4. +++ b/core/modules/system/src/Tests/Menu/LinksTest.php
    @@ -172,119 +172,26 @@ function testMenuLinkReparenting($module = 'menu_test') {
    -    // Start over, forcefully delete the parent from the database, simulating a
    -    // database crash. Check that the children of parent are now top-level.
    -    $links = $this->createLinkHierarchy($module);
    -    // Don't do that at home.
    -    db_delete('menu_links')
    -      ->condition('mlid', $links['parent']['mlid'])
    -      ->execute();
    -
    -    $expected_hierarchy = array(
    -      'child-1-1' => 'child-1',
    -      'child-1-2' => 'child-1',
    -      'child-2' => FALSE,
    -    );
    -    $this->assertMenuLinkParents($links, $expected_hierarchy);
    

    Do we need a new test to replace this?

  5. +++ b/core/modules/system/src/Tests/Menu/LinksTest.php
    @@ -172,119 +172,26 @@ function testMenuLinkReparenting($module = 'menu_test') {
    -  function testMenuLinkRouterReparenting() {
    

    Do we need a new test to replace this?

  6. +++ b/core/modules/system/src/Tests/Menu/LinksTest.php
    @@ -172,119 +172,26 @@ function testMenuLinkReparenting($module = 'menu_test') {
    -  public function testRouterIntegration() {
    

    Do we need a new test to replace this?

  7. +++ b/core/modules/system/src/Tests/Menu/LinksTest.php
    @@ -172,119 +172,26 @@ function testMenuLinkReparenting($module = 'menu_test') {
    +    // @todo - figure out what makes sense to test in terms of automatic
    +    //   re-parenting.
    

    Apparently, yes to some of the above :) Let's make sure we have an issue filed for that.

  8. +++ b/core/modules/system/src/Tests/Menu/MenuRouterTest.php
    @@ -53,10 +53,6 @@ public function testMenuIntegration() {
    -    $this->doTestMenuItemTitlesCases();
    -    $this->doTestMenuLinkMaintain();
    -    $this->doTestMenuLinkOptions();
    -    $this->doTestMenuItemHooks();
    

    Do we have sufficient test coverage for the stuff that replaced what these were testing?

  9. +++ b/core/modules/system/system.api.php
    @@ -397,13 +397,13 @@ function hook_page_build(&$page) {
    - * Alter links for menus.
    + * Alter all the menu links discovered by the menu link plugin manager.
    

    While we're here, s/Alter/Alters/.

  10. +++ b/core/modules/system/system.module
    @@ -1455,76 +1456,48 @@ function system_admin_compact_mode() {
    -      // Check the admin tasks for duplicate names. If one is found,
    -      // append the parent menu item's title to differentiate.
    

    Did we remove this functionality entirely for some reason or has it moved to somewhere else?

dawehner’s picture

All of these are just nits. I couldn't find anything significant to complain about :)

Yeah!!! Thank you for the intensive review.

Pushed the changes to the usual branch.

Difference between $tree and $menu_tree not clear from the variable names. Possibly a followup to improve local variable names?

Sure, here is one: #2309493: Improve $tree vs. $menu_tree variables

Is there a reason for this to be a static local variable rather than a protected instance variable?

I don't see one ... probably just c&p from the old code. I fixed this.

Here and elsewhere: when we make this change, do we still need 'localized_options', or should that be removed?

localized_options are certainly still in use for local tasks/actions. Here is a follow up to remove it in all places, if possible: #2309499: Drop 'localized_options' in all places?

Why is SafeMarkup::set() added to one of these and not the others?

You seem to just need a SafeMarkup::set() if there are multiple elements containing HTML. You don't need one if there is just one HTML string or multiple NON-html strings. Please clarify if I am wrong here. Expanded them as a form with indentation got rendered wrong.

This code is already inside a #pre_render. Why are we adding another pre_render layer rather than calling that function directly?

No idea, maybe cacheablity? wim?

Let's get a followup issue filed for this and link to it from here.

There is one: #2309501: Use plugin IDs instead of titles for toolbar classes

Doesn't look like this patch has any CSS changes included. Does this change require any?

Good question! I did a quick grep. There are several instances of ".links" selectors. Most of them are generic links, so they don't have to be changed.
I could not find any selectors which got targeted against the secondary menu or menu in general, but I could be totally wrong here.

Should "views" be capitalized?

Sure, let's do that.

Is this @todo still relevant? Looks like the linked issue is fixed, and views_menu_alter() no longer exists.

Yes, this code existed because the old menu system keyed by path, so overriding existing paths was more tricky than with the routing system, which keys by route name.

Also, we should add editing of description to both the functionality and then to this doc.

Added description, yeah let's just do it here.

If the fix for this is easy, let's add it to this patch. Otherwise, let's open a followup and add a link to the @todo.

Given that I am not even sure whether this is a problem we should have a dedicated test coverage here. Added #2309507: Ensure that menu link content/views is not loaded localized in the menu link edit form

Why do we need this check? If the plugin specified to use this form, can't we assume it implements the needed interface?

Well, this was just added for documentation purposes. Replaced it with documenting the property on the class.

How about making this $form['path']['#description'] and unsetting $form['info']?

I think it makes sense to explain the type of menu link in the exact same place. Feel free to disagree.

Any reason not to add them right now to this patch? If some of them are difficult, let's open a followup.

Done

Is there any code calling these? The implementation seems wrong to me. Since the plugin IDs are are all 'views_view:...', those if statements are never true.

Sadly I don't remember why this was maybe needed at some point. Let's drop it and add later, if it was really needed.

Since we have the {@inheritdoc}, we don't need the @return.

you are totally right.

This is a UX change/regression. For example, for the Block module, the Configure link now says "Block layout" instead of "Configure what block content appears in your site's sidebars and other regions.". I get the desire to decouple Configure links from menu links, but I wonder if this at least needs a followup issue to discuss further.

I do agree that this is a UX regression, let's also use menu links, which are even easier to use tbh. Sadly we can't rely on menu links here, so we also need the route logic. One example is the configure link of the block_content module.

At least in this patch, this seems to be a very common value for $manipulators. How about a followup to discuss making that a default for the transform() function in some way?

Sure, with #2250315: Regression: Bring back node access optimization to menu trees this list will be expanded even more. Follow up: #2309521: Provide a sane default for menu link manipulators.

Did we remove this functionality entirely for some reason or has it moved to somewhere else?

To be honest I cannot imagine that this was ever a real problem but I might be wrong.

Apparently, yes to some of the above :) Let's make sure we have an issue filed for that.

One reason these tests got dropped was that reparenting afaik this test not makes sense for content menu links.
Here is a follow up: #2309531: Add a test for re-parenting of menu links.

Wim Leers’s picture

This code is already inside a #pre_render. Why are we adding another pre_render layer rather than calling that function directly?

Well-spotted!

The reasons for this are:

  1. Maintainability We want a similar code all across Drupal for rendering menus; the previous code was very hard to follow. Now we have toolbar_prerender_toolbar_administration_tray() which can be easily compared with e.g. SystemMenuBlock::build().
  2. Cacheability We want to be able to cache all rendered menus once #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees, and the toolbar's admin menu tray is a significant contributor to authenticated page load slowness. By having this in a #pre_render callback already, it'll become a simple matter of setting a #cache property next to the #pre_render property, and *poof* — it'll be cached!

Doesn't look like this patch has any CSS changes included. Does this change require any?

No, no CSS changes are necessary.


There also still are the two bugs I reported in #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:

  1. The "Home" link in the main menu in a Standard install does not get marked as active while on the front page, due to missing the value for the data-drupal-link-systempath attribute).
  2. Menu links defined in YML files can be repositioned in the tree just fine. But whenever caches are cleared, the original position is restored! Or rather, this was the bug at the time of #2256521-107: [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 (almost a month ago). Now, it's gotten even worse: it'll lose the repositioned menu links in the menu where it belongs, and move it to a different menu instead!
    You can reproduce this by going to admin/structure/menu/manage/admin, drag the "Structure" subtree to be at the top level, save. All is well. Now go to admin/config/development/performance clear caches, then go back to admin/structure/menu/manage/admin and … be surprised by the total absence of the "Structure" subtree… :( … and then visit admin/structure/menu/manage/tools, where they've magically reappeared. WTF! :D
Wim Leers’s picture

FileSize
257.4 KB
20.08 KB
730 bytes

The first bug described in #41 is actually a bug in UrlGenerator. Rendering menu links now uses LinkGenerator instead of l(), hence the regression.

l() does this:
$variables['options']['attributes']['data-drupal-link-system-path'] = \Drupal::service('path.alias_manager')->getPathByAlias($path);

LinkGenerator::generateFromUrl does this:
$variables['options']['attributes']['data-drupal-link-system-path'] = $url->getInternalPath();

But when rendering the front page front page link, $url->routeName == '<front>', so $url->getInternalPath() calls UrlGenerator::getPathFromRoute() which calls UrlGenerator::getInternalPathFromRoute() with the "materialized route" (i.e. /, not <front>) and then consequently fails to return <front>.

I hope the solution I went for is correct — UrlGenerator is rather confusing. It does fix the bug I reported.

And yes, I know, ideally we'd change data-drupal-link-systempath to use routes & parameters rather than system paths, but that's a much bigger undertaking, hence making that vastly out of scope.


Attached is the latest version of part4, with all the fixes that dawehner did (interdiff_dawehner.txt) to fix #36/#37/#38 and the tiny fix I did for the first bug I reported (interdiff_wim.txt). I don't know where to begin to debug the second bug I reported — hopefully pwolanin/dawehner can fix that.

Status: Needs review » Needs work

The last submitted patch, 42: 2301317-42.patch, failed testing.

pwolanin’s picture

The bug with the rebuild seems to be the link getting moved to the tools menu

xjm’s picture

Assigned: xjm » effulgentsia

@effulgentsia is doing the line-by-line. :)

effulgentsia’s picture

FileSize
257.39 KB
pwolanin’s picture

Status: Needs work » Needs review

Haven't resolved the shortcut fail, but let's see if the bot is otherwise happy.

dawehner’s picture

@effulgentsia
Please also push the changes into the repo.

effulgentsia’s picture

Review for the menu_ui changes, excluding tests. Will do a review of the tests in my next comment.

  1. +++ b/core/modules/menu_ui/menu_ui.module
    @@ -183,113 +133,18 @@ function menu_ui_menu_delete(Menu $menu) {
    -  if (isset($menus[$menu_name])) {
    -    $build['#contextual_links']['menu'] = array(
    -      'route_parameters' => array('menu' => $menu_name),
    -    );
    +  if (isset($menus[$menu_name]) && isset($build['content'])) {
    +    foreach (Element::children($build['content']) as $key) {
    +      $build['#contextual_links']['menu'] = array(
    +        'route_parameters' => array('menu' => $menu_name),
    +      );
    +    }
    

    The added foreach doesn't appear useful.

  2. +++ b/core/modules/menu_ui/menu_ui.module
    @@ -330,29 +185,40 @@ function menu_ui_node_type_delete(NodeTypeInterface $type) {
    +    /** @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface $link */
    

    $link doesn't appear in the code below this.

  3. +++ b/core/modules/menu_ui/menu_ui.module
    @@ -378,67 +247,69 @@ function menu_ui_node_predelete(EntityInterface $node) {
    +        $menu_link = \Drupal::entityManager()->getStorage('menu_link_content')->load($id);
    

    Why not use the nice entity_load() wrapper?

  4. +++ b/core/modules/menu_ui/menu_ui.module
    @@ -378,67 +247,69 @@ function menu_ui_node_predelete(EntityInterface $node) {
    +    $form_state['menu_link'] = $definition;
    

    Would 'menu_link_definition' be a clearer key name?

  5. +++ b/core/modules/menu_ui/menu_ui.module
    @@ -548,18 +408,24 @@ function menu_ui_form_node_form_alter(&$form, $form_state) {
    +      // Have to tack this onto the node so we can save it later when we have a
    +      // a node ID for any new node.
    +      $node->menu = $definition;
    

    Although $node->menu is a preexisting condition of HEAD, can we open an issue and add an @todo here to remove that? In D8, we shouldn't be adding arbitrary non-field properties to content entities.

  6. +++ b/core/modules/menu_ui/menu_ui.module
    @@ -602,9 +470,8 @@ function menu_ui_form_node_type_form_alter(&$form, $form_state) {
       // To avoid an 'illegal option' error after saving the form we have to load
       // all available menu items.
       // Otherwise it is not possible to dynamically add options to the list.
    -  // @todo Convert menu_ui_parent_options() into a #process callback.
    -  $menu_link = entity_create('menu_link', array('mlid' => 0));
    -  $options = menu_ui_parent_options(menu_ui_get_menus(), $menu_link);
    +  // @todo Convert getParentSelectOptions() into a #process callback.
    +  $options = $menu_parent_selector->getParentSelectOptions('');
    

    Preexisting condition of HEAD, but I'm confused by this comment and @todo. I think it's made slightly worse with the conversion to use the selector, because I don't think it's intending to imply that some change is needed to that selector method itself.

  7. +++ b/core/modules/menu_ui/menu_ui.routing.yml
    @@ -21,37 +21,30 @@ menu_ui.parent_options_js:
    -  path: '/admin/structure/menu/item/{menu_link}/edit'
    +  path: '/admin/structure/menu/link/{menu_link_plugin}/edit'
    

    Do we need the "_plugin" suffix? Now that the conversions are done, wouldn't {menu_link} be unambiguously the plugin?

  8. +++ b/core/modules/menu_ui/src/MenuForm.php
    @@ -253,26 +217,30 @@ protected function buildOverviewForm(array &$form, array &$form_state) {
         // We indicate that a menu administrator is running the menu access check.
    +    $manipulators = array(
    +      array('callable' => 'menu.default_tree_manipulators:checkAccess'),
    +      array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
    +    );
         $this->getRequest()->attributes->set('_menu_admin', TRUE);
    

    Move $manipulators lower so it doesn't separate the code comment above from what it refers to.

  9. +++ b/core/modules/menu_ui/src/MenuForm.php
    @@ -384,32 +369,30 @@ protected function submitOverviewForm(array $complete_form, array &$form_state)
    +            // Hidden is a special case, the form value needs to be reversed.
    +            if ($field == 'enabled') {
    +              $updated_values['hidden'] = $element['enabled']['#value'] ? 0 : 1;
    

    Why did we rename the form field to "enabled" if the definition key is named "hidden"?

pwolanin’s picture

FileSize
259.23 KB
1.01 KB

I re-created the repo branch from Alex's patch plus added my menu name fix

Here's patch + interdiff.

The last submitted patch, 46: 2301317-46.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 50: 2301317-49.patch, failed testing.

effulgentsia’s picture

And the menu_ui tests:

  1. +++ b/core/modules/menu_ui/src/Tests/MenuLanguageTest.php
    @@ -59,21 +59,12 @@ function testMenuLanguage() {
         // Check menu language and item language configuration.
         $this->assertOptionSelected('edit-langcode', $edit['langcode'], 'The menu language was correctly selected.');
    -    $this->assertOptionSelected('edit-default-language-langcode', $edit['default_language[langcode]'], 'The menu link default language was correctly selected.');
    -    $this->assertFieldChecked('edit-default-language-language-show');
    

    s/and item language//

  2. +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -78,18 +80,21 @@ function testMenu() {
    -      $node = node_load(substr($item['link_path'], 5));
    -      $this->verifyMenuLink($item, $node);
    +      $node = node_load($item->getRouteParameters()['node']);
    +      $this->verifyMenuLink($item, $node, NULL, NULL, "node/{$node->id()}");
    

    Why are we verifying this by visiting the node page. Is the link not showing up on the front page for some reason?

  3. +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -490,42 +487,12 @@ public function testBlockContextualLinks() {
    -    // Test if moving a menu link between menus changes the bundle.
    

    So with the new architecture, we're losing this feature. I think that's ok, but we need to make sure that's documented somewhere (I guess in an 8->8 change record?). Also, menu_link_content module in its current form doesn't have a UI for managing bundles. Are we punting that to contrib, or plan on adding that in a core followup?

  4. +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -578,35 +541,35 @@ function addMenuLink($plid = 0, $link = '<front>', $menu_name = 'tools', $expand
    -  function verifyMenuLink($item, $item_node, $parent = NULL, $parent_node = NULL) {
    +  function verifyMenuLink(MenuLinkContent $item, $item_node, $parent = NULL, $parent_node = NULL, $path = '') {
         // View home page.
    -    $this->drupalGet('');
    +    $this->drupalGet($path);
    

    If we're keeping the new $path parameter (per above, I don't know if we need to), we should document it, and update the "View home page." comment.

  5. +++ b/core/modules/menu_ui/src/Tests/MenuTest.php
    @@ -648,58 +611,35 @@ function moveMenuLink($item, $plid, $menu_name) {
    -    // Reset menu link.
    -    $this->drupalPostForm("admin/structure/menu/item/$mlid/reset", array(), t('Reset'));
    -    $this->assertResponse(200);
    -    $this->assertRaw(t('The menu link was reset to its default settings.'), 'Menu link was reset');
    -
    -    // Verify menu link.
    -    $this->drupalGet('');
    -    $this->assertNoText($title, 'Menu link was reset');
    -    $this->assertText($old_title, 'Menu link was reset');
    

    Looks like this was the only place testing the /reset route via the UI. Can we add back a test that does that?

dawehner’s picture

The added foreach doesn't appear useful.

Yeah, no idea how this happened.

$link doesn't appear in the code below this.

Renamed to $entity to make it useful.

Would 'menu_link_definition' be a clearer key name?

Agreed and changed

Although $node->menu is a preexisting condition of HEAD, can we open an issue and add an @todo here to remove that? In D8, we shouldn't be adding arbitrary non-field properties to content entities.

Opened a follow up: #2310173: Don't add arbitrary non-field menu to $node

Preexisting condition of HEAD, but I'm confused by this comment and @todo. I think it's made slightly worse with the conversion to use the selector, because I don't think it's intending to imply that some change is needed to that selector method itself.

Should we open just another follow up?

Do we need the "_plugin" suffix? Now that the conversions are done, wouldn't {menu_link} be unambiguously the plugin?

Just wondering whether _plugin makes it easier to disinct between an ordinary menu link and a menu_link_content

Move $manipulators lower so it doesn't separate the code comment above from what it refers to.

Good catch!

Why did we rename the form field to "enabled" if the definition key is named "hidden"?

We do want to have enabled/status at the end everywhere because it is easier to get. The node menu form got rewritten so we just did that.
On top of that the UI was always with an enabled checkbox.

s/and item language//

Wow!

Why are we verifying this by visiting the node page. Is the link not showing up on the front page for some reason?

It still passes without it ... i guess this was just needed temporarily.

So with the new architecture, we're losing this feature. I think that's ok, but we need to make sure that's documented somewhere (I guess in an 8->8 change record?). Also, menu_link_content module in its current form doesn't have a UI for managing bundles. Are we punting that to contrib, or plan on adding that in a core followup?

We decided to move this to contrib as you might want to have bundle per menu or not, depending on your usecase of fields.

Looks like this was the only place testing the /reset route via the UI. Can we add back a test that does that?

Will work on that later during the weekend

YesCT’s picture

Issue summary: View changes

is the itemized list in the proposed resolution in the summary an overview of all the parts so far 1-4, or just the changes in part 4? which should it be?
--
saving this again since my first save was lost...

pwolanin’s picture

Tabulating responses to each of the review points from @effulgentsia at https://docs.google.com/spreadsheets/d/1EPoJkSOG8R0NmFlY7adVXaoZgPvKudSA...

pwolanin’s picture

Status: Needs work » Needs review
FileSize
14.84 KB
264.83 KB

re: #49.7: The menu link content module creates routes with paths like: /admin/structure/menu/item/{menu_link_content}/edit
so, I guess it could be clear that {menu_link} -> MenuLinkInterface, but maybe we can discuss in a follow-up? Also, in this step we still have other routes that load {menu_link} as the old entity.

Also re: #49.9 there is already a follow-up to fix this to a status field which is referenced in the @todo in core/modules/menu_link_content/src/Entity/MenuLinkContent.php. #2305707: MenuLinkContent disabled field should be renamed to enabled and use positive logic

Also added a missing hook_help as pointed out by @tim.plunkett in IRC.

re: #53.5 dawehner is working on adding test coverage. I think all other review points have been addressed.

pwolanin’s picture

FileSize
265.86 KB
3.65 KB

re: #49.6 added a follow-up at https://www.drupal.org/node/2310319 and re-worked the code comment to include a link to that.

re: #53.5 this also has dawehner's added test for reset functionality.

The last submitted patch, 57: 2301317-57.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 58: 2301317-58.patch, failed testing.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
265.41 KB
5.84 KB

Better fix for the active class - handles it in the LinkGenerator and restores the test. plus a bunch of follow-up links added to @todos by YesCT.

(last local commit: c6092ed7c7036e4)

jibran’s picture

Issue tags: +JavaScript
pwolanin’s picture

Issue tags: -JavaScript

There are no JS changes relative to 8.x now.

YesCT’s picture

Issue summary: View changes
Issue tags: -Needs followup

todo's all have issues. updated remaining tasks and summary in the summary.

YesCT’s picture

Issue summary: View changes

html.

Status: Needs review » Needs work

The last submitted patch, 61: 2301317-61.patch, failed testing.

effulgentsia’s picture

All the increments and followups look good: thanks! I think this can be RTBC once tests are fixed and change records are written/reviewed. Also, minor but:

  1. +++ b/core/modules/views/src/Plugin/Menu/Form/ViewsMenuLinkForm.php
    @@ -0,0 +1,82 @@
    + * This provides the feature to edit the title, in contrast to the static
    

    and description :)

  2. +++ b/core/modules/views/src/Plugin/Menu/Form/ViewsMenuLinkForm.php
    @@ -0,0 +1,82 @@
    +    $form['description'] = array(
    +      '#type' => 'textfield',
    +      '#title' => $this->t('Description'),
    +      '#description' => $this->t('Shown when hovering over the menu link.'),
    +      // @todo how do we ensure that view is not loaded with a translation,
    +      //   see https://www.drupal.org/node/2309507
    +      '#default_value' => $this->menuLink->getTitle(),
    

    ->getDescription().

effulgentsia’s picture

We decided to move this to contrib as you might want to have bundle per menu or not, depending on your usecase of fields.

From the issue summary of #1966298: Introduce menu link bundles per menus:

So long as menus only have one single bundle, you cannot make certain menus multilingual while others are single language and you cannot hide/show language selectors per menu and you cannot make menu items translatable on a per-menu basis. You can only set these settings globally for all menus. This is not very realistic for multilingual sites.

So I left a comment in that issue asking for comment here. I'm ok with punting that to contrib if the D8MI team is, but I'm curious to know what Gábor and others who worked on that issue think.

pwolanin’s picture

Status: Needs work » Needs review
Related issues: -#1966298: Introduce menu link bundles per menus
FileSize
269.87 KB
13.19 KB

Silly me - the ternary in LinkGenerator was wrong except on the front page. 1 line fix, should fix all those fails.

Also fixed ViewsMenuLinkForm per #67 and doc and test fixes from YesCT, and made the MenuLinkBase more clearly abstract by moving the implementations of getTitle() and getDescription() to MenuLinkDefault. That change due to trying to write up developer examples and realizing that would be a better pattern.

@effulgentsia - Gabor reviewed these changes in terms of multilingual a while back including that change. Using the menu name as the bundle makes absolutely no sense for a filedable entity - it's just not supportable. The text from that issue is basically not really relevant given the changes we are making. If you want a menu full of non-translated links you'll have lots of options to achieve that.

pwolanin’s picture

This change record needs to be updated still: https://www.drupal.org/node/2226481

tim.plunkett’s picture

+++ b/core/lib/Drupal/Core/Menu/MenuLinkBase.php
@@ -40,18 +40,7 @@ public function getWeight() {
+  abstract public function getTitle();

@@ -112,12 +101,7 @@ public function isDeletable() {
+  abstract public function getDescription();

@@ -190,6 +174,11 @@ public function getTranslateRoute() {
+  abstract public function updateLink(array $new_definition_values, $persist);

You can just delete these. The class is abstract, and these methods are on the interface.

pwolanin’s picture

@tim.plunkett - we were debating that question in IRC. I think it's better to have them there to document that they need to be implemented, and we were not sure we have a clear coding standard.

tim.plunkett’s picture

when to use abstract:

1) protected methods
2a) public methods on traits
2b) public methods on classes with no interface [bad!]

There is no reason to use it when its backed by an interface and its public. We don't have these anywhere I could find, no reason to introduce them here.

YesCT’s picture

I finished reading through the patch. (Did *not* read every line of every test carefully.) Fixed a bunch of nits, docs, namings, and a few typehints in the sandbox.. and addressed #71. I guess this is ready for a new patch and interdiff when @pwolanin gets a chance.

On to manual testing.

YesCT’s picture

0a. These could all be addressed in (multilingual) follow-ups... (in part because for each thing that does need work, it will need a test.
0b. do not let #2188675: "Translate" local task always visible, leads to WSOD distract from this issue. To test translation, enable content translation. (with that bug, you will see translate tabs even though you should not)

1. Problem:
The translate tab/page for custom menu links is not in the admin theme. Maybe we can do something similar to #2276387: Translate routes should properly inherit admin path use from edit route here or in a follow-up.

2. Translations
2a. As expected:
plugin/discoverable menu links are translated using User interface translation (locale, po files, po overrides)
2b. As expected:
custom menu links (menu link content entities) are translated by installing the content translation module.
2c Concern:
Where can we add to the help that for Menu links (not Custom menu links), use the User interface translation?

3. (unrelated, nothing wrong, but noting) There is a language select on menus (menus are config) (when language module is installed):

4. Language of menu links...
4a. Working as expected:
There is a language default select on the content language overview for custom menu links

4b. Working as expected:
there is no language select on individual menu links until Show language selector on create and edit pages is selected on the content language settings page for custom menu links.
4c. Problem:
Creating a menu link, then showing the language selector, and then editing the menu link and selecting a specific language
[which is effectively changing the language of a custom menu link from not specified to en]
gives:
"Site-Install
Error
The website has encountered an error. Please try again later."
and does not save the language of the menu link.
Not great.
4d. [edit] Problem Works as expected:
Creating a menu link when there is a default language for custom menu links (for example en), works ok. Changing the language (to say af) does save that menu link as af.

5a. Working as expected
enabling configuration translation makes *menus* have a translate operation. (No need to enable translation for menus, all config is translatable once config translation module is installed).

6. Turning on content translation
6a. Problem:
unsets the language select defaults for custom menu links. :(
(for example, the show language selector got turned off)

7. Translating custom menu link has several nice things.

7a. page title like "Create English translation of Two af" just works and gives nice context.
7b. setting certain fields as not translatable (for example description), gets the (All languages) on the form.

8. Problem
editing a custom menu link and unchecking show as expanded, does not save. Reloading the edit page still has show as expanded checked.

10. Custom menu link module help (menu_link_content_help)

10a. I think we need to take out the bit about translation (because multiple languages is not (should not be) enough to translate once #2188675: "Translate" local task always visible, leads to WSOD is fixed).
10b. "it" reads a bit awkward and ambiguous.
10c. Maybe some paragraph tags to split up ideas would help.

11. Works as expected:
Reordering menu links in a menu

12. Works as expected
Moving a menu link to another menu

13. Works as expected
Reseting a (plugin/config/non-content/non-custom) menu link.

14a. Concern
if a custom menu link was created before language select was set, then later translation was enabled, the delete form is a bit odd:

14b. Works as expected
Delete form for a translation made after turning on language and translation is fine.

15. Concern
Custom menu link that is translated is not showing translated when the path of the page displayed is in that language
(in my test set up, Four af, should have been shown as the menu title on a af page)

100. I had thought that language select on nodes (or whatever) was typically last on the edit form, but that is not the case. For nodes, language select is after title and before body. For custom menu links it is after: title, path, description, and before the enable checkbox.

101. Possibly unrelated problems
101a. deleting the translation of an article get the website encountered error.
101b. editing a translation of a node gets same error.

-----
Summary.
Should try these again, with definite steps to reproduce.

dawehner’s picture

Thank you for that late manual testing!

The translate tab/page for custom menu links is not in the admin theme. Maybe we can do something similar to #2276387: Translate routes should properly inherit admin path use from edit route here or in a follow-up.

This is not a follow up: #2310475: Admin theme for translation tabs just works for nodes

Where can we add to the help that for Menu links (not Custom menu links), use the User interface translation?

Question: How do we do that for nodes?

Creating a menu link, then showing the language selector, and then editing the menu link and selecting a specific language
[which is effectively changing the language of a custom menu link from not specified to en]
gives:
"Site-Install
Error
The website has encountered an error. Please try again later."
and does not save the language of the menu link.

Please ALWAYS show errors (configured on admin/config/development/logging). Sadly drupal does not do that any longer by default.

I tried the same and it worked fine. I used two languages: en and fr (both content translation installed and uninstalled).

4d. does not sound like a problem, right?

6a. Problem:
unsets the language select defaults for custom menu links. :(
(for example, the show language selector got turned off)

This sounds like a content translation bug, doesn't it?

editing a custom menu link and unchecking show as expanded, does not save. Reloading the edit page still has show as expanded checked.

Note: The checkbox should be marked as disabled given that it is for "all languages". Is there an issue for content translation already
to do that?

if a custom menu link was created before language select was set, then later translation was enabled, the delete form is a bit odd:

Isn't that the same for node?

Custom menu link that is translated is not showing translated when the path of the page displayed is in that language
(in my test set up, Four af, should have been shown as the menu title on a af page)

Fixed that, ... this was just a caching bug.

dawehner’s picture

... multipost

pwolanin’s picture

FileSize
270.35 KB
15.33 KB

new patch with fixes from dawehner and YesCT for the issues they and tim,plunkett found, plus flags the menu link content edit route as admin which I think will resolve the issue YesCT found of it not being in the admin theme (seems to work for me locally).

YesCT’s picture

Thanks for the reminder to always show errors (admin/config/development/logging). I had looked in the apache log, but forgot about that new-ish setting.

#75
1. fixed. translate tab in non-admin theme.
2c. Creating separate issue. Where to document the 3 different ways of translating menu things. Config translation for menus. User interface translation for default menu links. Content translation for Custom menu links. #2310639: Document (expose) how to enable translation for Menu Things (Menus/Configuration translation, (Plugin) menu links/User interface translation, Custom menu links/Content translation)
4c. Having trouble reproducing this one. But was able to get an error another way: Enable content translation for Custom menu links. Create a custom menu link. Add a new language. Go to the translate page for that menu link. Click add to add a translation for that new language. Error is "invalidArgumentException: Invalid translation language (fi) specified". Issue for that: #2310647: Allowed language langcode argument cached cause Error
6a. could not reproduce
8. - still a problem. Enable content translation for Custom menu links. Add a custom menu link, leave show as expanded unchecked. Save. Edit. Show as expanded is checked.
10. can be done later as part of 2310639 or a different separate issue.
14a. could not reproduce the strange delete confirmation page
15. I'll be back in a bit to verify that is fixed.

YesCT’s picture

15. verify that it is fixed. custom menu links are showing the translated titles both in the navigation and in blocks in the sidebar.

For the interested, the fix was:

+++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
@@ -131,7 +131,7 @@ public function getCacheTags() {
   protected function getRequiredCacheContexts() {
     // Menu blocks must be cached per role: different roles may have access to
     // different menu links.
-    return array('cache_context.user.roles');
+    return array('cache_context.user.roles', 'cache_context.language');
YesCT’s picture

I can also verify that selecting the source for the translation works the same as for nodes.

What is a bit different is the author and date. in Menu links it is in the translation collapsed field set, and in nodes it is it's own field set (on the side/in the vertical tabs)
#2310657: Properties/Fields like author and date for Custom menu links translations

YesCT’s picture

Issue summary: View changes

marking manual testing as done.

YesCT’s picture

8. fixed in the sandbox with

+++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
@@ -211,7 +211,7 @@ protected function extractUrl($url) {
    */
   public function extractFormValues(array &$form, array &$form_state) {
     $new_definition = array();
-    $new_definition['expanded'] = !empty($form_state['values']['expanded']) ? 1 : 0;
+    $new_definition['expanded'] = !empty($form_state['values']['expanded']['value']) ? 1 : 0;
dawehner’s picture

Issue summary: View changes
FileSize
1.42 KB
270.95 KB

Fixed the expanded problem and added a TODO

Status: Needs review » Needs work

The last submitted patch, 84: 2301317-82.patch, failed testing.

xjm’s picture

Looks like #83 could use explicit test coverage but also like it broke something else? :)

pwolanin’s picture

Status: Needs work » Needs review
FileSize
271.28 KB
1.4 KB

dawehner and I worked through this bug - turn out the fix he made for expanded form values broke the test since all links had been getting expanded before and hence appearing on the home page unexpectedly. This is why we were checking the $path instead of home page in earlier version, but changing that from $path back to '' in response to #53 + the actual form values fix, created a test bug.

Easiest fix is to go back to marking the parent links expanded. Also fixes up some confusing code comments.

effulgentsia’s picture

Assigned: effulgentsia » Unassigned
Related issues: +#2311295: Introduce MenuLinkContent bundles

All the increments from #69 on look good to me, so I'm happy with the state of the code and docs in the patch, and am therefore, unassigning myself.

For the MenuLinkContent bundle per menu discussion, I opened #2311295: Introduce MenuLinkContent bundles, which we can discuss as a followup. The reason I think it's okay as a followup is that the relevance of bundles is for either fields or for translation, and in HEAD, menu links are neither fieldable nor translatable, so losing the bundles is not a regression. But, given #1966298-124: Introduce menu link bundles per menus, I think it's worth having as a core followup to keep discussing.

I'm happy with this being RTBC as soon as:
- YesCT confirms that all of her feedback from manual testing has been adequately addressed.
- xjm confirms that the change records are in a good enough state to not need to block this issue being RTBC.
- YesCT, xjm, others: what about test coverage for the various fixes from #75 and later? Are any of those things that need new coverage?

YesCT’s picture

Issue summary: View changes

I'm good with the feedback and manual testing results.
child issues are good to cover things
#2310475: Admin theme for translation tabs just works for nodes can add tests for the admin theme on translate link situation as it will also take out the fix stuck in here.
also added
#2311449: Add tests for menu links not expanded on front page and other pages
#2311451: Add test for menu links and custom menu links are rendered translated in both nav menu and a menu block
just to make sure.

re-editing the issue summary to re-mark manual testing task as done

so. I'm good.
next, pwolanin and I are working on the change records

YesCT’s picture

Issue summary: View changes

clarifying in the summary.

pwolanin’s picture

FileSize
271.28 KB
610 bytes

trivial doc fix

YesCT’s picture

Issue summary: View changes

change record list added to the issue summary with link to the revisions to review. (the revisions links would need updated as further changes to them are made that this issue missed so far)

[edit] ug. sorry about the file removed from the recent files table.

YesCT’s picture

Issue summary: View changes

noting that a complete search was done to find all change records that need an update.

pwolanin’s picture

see #91 for latest patch with passing test run at https://qa.drupal.org/pifr/test/833188

cross-post seems to have confused d.o

YesCT’s picture

Issue summary: View changes
effulgentsia’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
271.28 KB

Reuploading #91 to get pifr's green background color.

I don't know if more polish is needed on the change records, but even if yes, I think that can happen in parallel with people monitoring the RTBC queue having a chance to review the patch. Therefore, RTBC. Awesome work, everyone!

xjm’s picture

Assigned: Unassigned » alexpott
Issue summary: View changes

I'm good with YesCT's followups. I'd still like to confirm that all the dangling points from previous issues have been addressed or had their own followups, but I will... follow up with that later.

Ideally alexpott would review this since he reviewed the others and had some of those previous bits of feedback... (sorry Alex).

Also adding myself to the commit message as I spent an entire day trawling through CRs (with help from dawhener, pwolanin, and effulgentsia) and I'm still not done. But okay with RTBC on the assumption that this patch takes a bit to marinate and review anyway (and because I've resigned to doing the needed changes myself).

alexpott’s picture

Assigned: alexpott » Unassigned
Issue summary: View changes
FileSize
271.31 KB

Oops I broke the patch... rerolled.

xjm’s picture

Assigned: Unassigned » alexpott
Issue summary: View changes

Nice try, bub. ;)

alexpott’s picture

Assigned: alexpott » catch
Issue summary: View changes

@xjm: I actually think it would be great if catch takes this one - more eyes! I still plan to have a good look at the patch. Chatted with catch already and he seems okay with this plan.

xjm’s picture

Issue summary: View changes

Assuming this is just continued crossposting and not "screw you, xjm, no one values your work"... :P

xjm’s picture

Issue summary: View changes
pwolanin’s picture

Version: 8.x-dev » 8.0.x-dev

8.0.x please

xjm’s picture

Update yer bookmarks!

xjm’s picture

Having two change records entitled "Menu links converted to plugins..." was super confusing, so I've incorporated the old one into the new one:
https://www.drupal.org/node/2302069

We can delete the old one (https://www.drupal.org/node/1914008) after the new one is published.

Still need to document the upgrade for a couple more constants, and then improve the introduction/overview a bit.

catch’s picture

Overall this looks great. One nit-pick, one real question. Didn't see anything else that would hold up commit, everything that looked a bit off has a @todo and issue linked.

  1. +++ b/core/includes/menu.inc
    @@ -367,21 +405,40 @@ function _menu_link_translate(&$item) {
    +  if (!isset($variables['attributes']['class'])) {
    

    Very minor but $variables['attributes']['class'][] = 'menu' should just work whether it's set or not without throwing a notice.

  2. +++ b/core/modules/menu_link_content/menu_link_content.install
    @@ -0,0 +1,27 @@
    +  // Find all the entities and then call the manager and delete all the plugins.
    

    Really? Won't that be OOM on lots of sites?

catch’s picture

Status: Reviewed & tested by the community » Fixed

Opened #2312389: Remove menu_link_content_uninstall().

Committed/pushed to 8.0.x, thanks!

  • catch committed fd2db9c on 8.0.x
    Issue #2301317 by pwolanin, effulgentsia, Wim Leers, dawehner, alexpott...
xjm’s picture

Published https://www.drupal.org/node/2302069 and deleted the old one.

dsnopek’s picture

Huzzah! Congrats to everyone who's been pushing this epic issue forward for so long. :-)

alexpott’s picture

I reverted and re-committed to bring the commit credits up to date with the suggested commit message in the summary.

  • alexpott committed 44f76c6 on 8.0.x
    Revert "Issue #2301317 by pwolanin, effulgentsia, Wim Leers, dawehner,...
  • alexpott committed 70bed33 on 8.0.x
    Issue #2301317 by pwolanin, dawehner, Wim Leers, effulgentsia, YesCT,...
Wim Leers’s picture

pwolanin’s picture

re: #106 - we are not deleting the entities, just the plugins, but yes - uninstalling menu link content on a site with larges abounts of that entity would be slow or OOM - but are people going to do that?

Also, _menu_link_translate() is deleted in part5, so was that just showing as context?

cosmicdreams’s picture

FileSize
80.67 KB

Hey gang, tested HEAD on Simplytest.me in order to see if I could find the UI changes. Found a bug instead. Think it might be this patch that introduced the regresssion:

markup exploded in the UI

xjm’s picture

@cosmicdreams, that looks actually like a double-escaping issue from the Twig autoescape change. In any case, please post a separate issue with steps to reproduce, as a child issue of #2297711: Fix HTML escaping due to Twig autoescape.

xjm’s picture

Issue tags: +beta blocker

Tagging the child issues retroactively.

Status: Fixed » Closed (fixed)

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

Xano’s picture