Problem/Motivation
As part of #2162013: Critical performance regression due to config schema + TypedData lookups in installer I noticed that we are rebuilding the router every time we save a view. This means that whilst enabling views we rebuilding the router 9 times during config_install_default_config which takes 4.7 seconds on my computer!
xhprof from enabling views using drush
Proposed resolution
Instead of rebuilding the router you now mark the router to be rebuilt. Once something requests the route information it rebuilds automatically.
In case nothing requests route information we rebuild the route on the terminate event, which happens after sending the response to the user.
The router rebuild flag is stored in state in case anything happens in between marking the router needing a rebuild and the prevents the terminate event from firing.
Whilst working on this issue I discovered that we currently have dependencies between the router and the menu router. For example the route listener is views sets a state flag that is using in views_menu()
. Therefore this issue adds an event subscriber that ensures a menu router rebuild when the router is rebuilt and replaces anywhere in the code that sets the menu_rebuild_needed
flag with a call to RouteBuilder::setRebuildNeeded()
Remaining tasks
Review code.
User interface changes
None.
API changes
- RouteProvider depends on RouteBuilder and listens to the router rebuild event to invalidate its static caches using the new method
resetStaticCache()
. - RouteBuilder depends on state and has two new methods
rebuildIfNeeded()
andsetRebuildNeeded()
. It implements the newRouteBuilderInterface
.
Comment | File | Size | Author |
---|---|---|---|
#50 | 2164367.50.patch | 54.25 KB | alexpott |
#47 | 2164367.47.patch | 54.1 KB | alexpott |
#47 | 38-47-interdiff.txt | 9.5 KB | alexpott |
#38 | 37-38-interdiff.txt | 1.41 KB | alexpott |
#38 | 2164367.38.patch | 54.31 KB | alexpott |
Comments
Comment #1
alexpottComment #2
alexpottComment #3
alexpottDiscussed with @timplunkett in IRC we can now remove the rebuild because the following code ensures that the router will be rebuilt.
#2089635: Convert non-test non-form page callbacks to routes and controllers added the rebuild to menu.inc if this state key/value was set. According to @timplunkett this was added because tests failed without it so I believe we have test coverage that setting this will cause a router rebuild.
Image below shows the same xhprof as in the issue summary after applying the patch.
Comment #4
xjmComment #6
tim.plunkettI think this used to work in most cases, but unfortunately menu_get_item() is not guaranteed to be called on every page, so it's not always triggered...
And unless it is, the assertions will only happen on the second request, not the first.
I would have thought that the second request of a drupalPostForm() would handle it though?
Just trying this out for now.
Comment #8
tim.plunkettMight as well get this passing, even if we don't go this way...
Comment #10
alexpottMaybe this will work.
Comment #11
alexpottComment #13
tim.plunkettI guess this also needs the same check you're deleting:
Comment #14
alexpottHow about this? No need for the @todo - fingers crossed :)
Comment #16
tim.plunkettSome unit tests already were installing the router table, removed the duplication.
This fixes the unit tests, but added back in a couple fixes for web tests.
Comment #17
dawehnerIf you save a view you want to ensure that a possible change does get reflected in the routing definition, at least in the UI.
Other places which might need to be touched: AdvancedSettingsForm.
Comment #18
dawehnerMaybe forget my last patch, though not rebuild immediately opens new potential bugs, like a missing router definition, when the menu items are setup.
This is really fragile.
Comment #20
alexpottThis adds the ability to check if the router needs a rebuild and makes the route provider check this before providing routes.
New approach therefore not bothered with an interdiff.
Comment #21
tim.plunkettThis essentially obsoletes #2167323: \Drupal::state()->set('menu_rebuild_needed') is not guaranteed to be checked on the next request.
If we choose to go this way, we should also remove the hunk from menu.inc (and close that and make this critical).
Comment #22
dawehnerThe documentation is a bit odd, as I would understand that FALSE means the route rebuild failed. Maybe something like: "Returns TRUE if the rebuild was done, FALSE otherwise."
Is there any reason why we mark the router builder here as optional?
Comment #23
BerdirOptional would be @?router, that's just a string?
Comment #25
dawehner@berdir
Yes, but I think there is no reason that this was intended, given that we just call methods on the object.
Comment #26
alexpottOne good thing about using state rather than a termination event as suggested by #2167323: \Drupal::state()->set('menu_rebuild_needed') is not guaranteed to be checked on the next request is that after the state is set if the route provider is used again in the same request the router will be rebuilt. Although I agree that in addition to these changes we could ALSO add a termination event to rebuild the router if required since this will not happen on user time.
Comment #28
alexpottOh well
Comment #29
tim.plunkettMarking #2167323: \Drupal::state()->set('menu_rebuild_needed') is not guaranteed to be checked on the next request as a dupe of this one. Haven't come up with a better title, but this is bigger than just views now.
Comment #32
sunThe amount of conditionals being injected into plenty of spots here looks extremely concerning, hard to follow, and fragile.
In #2167323-6: \Drupal::state()->set('menu_rebuild_needed') is not guaranteed to be checked on the next request, @catch pointed out that this entire (ugly) rebuild-needed state flag was originally introduced in 2007 for D6 in:
#202955: Access denied after install - menu_rebuild() calls
The variable/state flag was introduced despite @Dries clearly objected to it back then already.
The original scenario of dynamically defined routes in hook_menu() no longer exists (in the offending form) in D8.
In addition, the very original root cause no longer applies, since the Drupal installer performs module installations in a full and regular Drupal environment today.
A rebuilt router is only required upon first route lookup/access. That could possibly happen (1) in a hook_install() implementation (albeit rare) or (2) when outputting a link in a message to a newly saved entity that is based on a dynamically defined route (also rare).
What we want to achieve is to (re)build the router only once within a single request, regardless of how many modules are being installed and regardless of how much configuration and how many entities are being saved.
Therefore, I'd recommend to do this:
Comment #33
jhodgdonThe title of this issue has "views" in it... but its scope is wider. tim.plunkett says it's the cause of #2042807-111: Convert search plugins to use a ConfigEntity and a PluginBag as well. This issue might need a new title and a better summary?
Comment #34
alexpottIssue summary update will happen.
Fresh start on the patch using a termination event. I believe we should be still be using state to store the fact the a router rebuild needs to occur. Anything could occur between setting the flag on the the route builder and terminate event. Using state ensures that a rebuild will occur.
I've also tied the menu_router_rebuild to the router rebuild and removed the ability for menu_get_item to rebuild the router (unless it is necessary).
Implemented a config.save event listener to set the router rebuild needed if the active plugins in search.settings change. I think we should employ this pattern more because it means that we can guarantee behaviour if things change.
There will be atleast 2 fails in StandardTest - at the moment I have nfi why.
The patch is kind of a start again so no interdiff.
Comment #36
dawehnerThere is no need to rebuild it twice.
I wonder whether it is enough to check just for Container
This change seems to come from a total different patch.
Some documentation which explains that this ensures the routes being there when rebuilding the menu router. I really like the idea.
One of these todos seems to be enough.
I kind of prefer to call the public count method directly.
I don't really see a requirement to rebuild the router really early, can you maybe document why this is needed?
Comment #37
alexpottMassive reroll following search pluginifcation - means we don't need the config listener anymore or the changes to config. Unfortunately an interdiff is not really possible.
One thing I do not yet understand is why we need to rebuild the menu router in standard.install in order to get the menu link it creates in the main menu to save properly and StandardTest to pass.
Comment #38
alexpottSo the rebuild of the menu router was necessary because enabling the theme earlier in standard profile was scheduling a router rebuild. This route rebuild was causing a menu router rebuild which deletes all menu links without a router_path which will the case if they are in the process of being created. MenuLinkStorageController::save() creates a dummy row to get an mlid early on.
Whilst scanning all the MenuLink code also noticed we are unnecessarily doing router lookups for external urls. Which will cause an unnecessary router rebuild during standard_install because it can't find the route and this triggers a route rebuild if necessary - which is because we've just enabled themes but it does not need to occur here.
Comment #39
sunThe new approach looks much much better + cleaner — awesome!
A static cache appears to be specific to the default implementation -- as long as the method is only called internally, we don't need to add it to the interface?
I'm not sure I understand why we're still rebuilding the router after enabling/disabling a theme?
D7 and below only required this, because hook_menu() (e.g., block_menu()) was defining routes based on the list of enabled themes. But in D8, that no longer exists.
The only part that still exists are local tasks (and possibly menu links), but those are not routes? Thus, we shouldn't have to rebuild the router; we just need to trigger a rebuild of local tasks/menu links...?
That said, happy to ignore this here and re-evaluate that situation in a separate follow-up issue.
I wonder why menu_install() has to manually trigger/force a rebuild?
Hm. I'm very concerned about this addition.
@alexpott explained in IRC that this is needed if setUp() has done something that sets the state. However:
The sole responsibility of TestBase::run() is to dispatch test runs. It is not supposed to contain functional test environment logic. All of that code lives in prepareEnvironment() & Co, and more specifically, setUp().
TestBase::run() is executed for any kind of test, including unit tests. The comment refers to a Simpletest test (testing the test runner), which is not only a web test, but additionally, all the Simpletest tests are extraordinarily special beasts → if this code is needed for those tests only, then we should move it into affected test classes (or introduce a base SimpleTestWebTestBase, which would be a good idea anyway).
Any particular reason for why is this instance setting the state flag manually/differently?
Comment #40
sunbtw: re: #38:
A single dummy row using an empty path doesn't sound thread-safe? Multiple users creating new menu links at the same time will blow up? Shouldn't we use at least a path of 'temp/hash(rand())' or similar there?
Comment #41
BerdirA quick comparison of this using
php core/scripts/run-tests.sh --url http://d8/ "Views module integration"
is 11m35 (HEAD) vs 10m24 (this patch).Comment #42
damiankloip CreditAttribution: damiankloip commentedGenerally this patch is looking great.
typo there. Also, is rebuild() ever called now?
Couldn't this logic just be implemented around the one original @todo? Just needs to set the rebuild flag in memory?
Looks like a strange sentence, can't we just say 'Tests the rebuildIfNeeded method' or something?
This just does tag clearing for the default cache bin, we need to use Cache::deleteTags() instead here. It's just coincidence they share the cache_tags table. If anyone switches implementations, this could break things.
I guess that means you also don't need to inject the cache backend?
Should we be doing this at the end of a request? as opposed to the start of a request? I guess that a 'six of one, half a dozen of the other' type argument.
EDIT: talking to dawehner, he pointed out this will technically get fired after the response has been sent, so is definitely the place to be.
Also, please rename this issue. It is not really an accurate description of what the patch is doing anymore :)
Comment #43
jhodgdonIssue needs title and summary that reflect what it is actually addressing. Neither is remotely accurate.
Comment #44
dawehnerComment #45
dawehnerUpdated the issue summary.
Comment #46
alexpottComment #47
alexpott#39.1
resetStaticCache()
is called from an event listener so I think it needs to be public - I've renamed it toreset()
so it is less implementation specific.#39.2 let's explore what enabling a theme needs in a followup :)
#39.3 otherwise the menu link entities it creates fails because the routes do not exist.
#39.4 I've removed this from the patch attached
#39.5 Changed to call the method on the router rebuild service.
#42.1 Fixed and yep
rebuild()
is called fromdrupal_flush_all_caches()
#42.2 Fixed
#42.3 Fixed
#42.4 Fixed
#42.5 @dawehner was right :)
Comment #48
dawehnerWow, this got stuck for quite a while.
Comment #49
damiankloip CreditAttribution: damiankloip commented+1 on that
Comment #50
alexpottRerolled
Comment #51
catchWith local tasks being split out, I wonder whether we really need to do this any more, this is just a straight switch here but might be worth a follow up to try to factor that out.
Couldn't find anything to complain about in the patch itself, this clears up a lot of crap.
Committed/pushed to 8.x, thanks!
Comment #52
xjmTagging "Missing change record", as completing the change record updates here blocks a beta: #2083415: [META] Write up all outstanding change notices before release
Comment #53
dawehnerhttps://drupal.org/node/2185947
Comment #54
tim.plunkettUpdated that one.
Comment #55
xjmThanks!
Comment #56
xjm