Problem/Motivation
There are cases when we should not rebuild the routes when saving or deleting a view:
- You have a view that only has a block
- You have a view with a page display, but you are not changing any option that would require a menu rebuild
The page display should be smart enough to be able to tell if the path(s) have changed. If paths or menu information has not changed, or it has no page displays at all, then menu should not be rebuilt.
Proposed resolution
- Deprecate views_invalidate_cache() and split its logic in \Drupal\views\Views::invalidateCache() and \Drupal\views\Views::routeRebuildNeeded()
- Introduce a new plugin method is called when the view is saved. The method should make decision whether to rebuild the routes.
- Don't ask route rebuilding on each save, rather call each plugin to make decisions.
Remaining tasks
None.
User interface changes
None.
Introduced terminology
None.
API changes
New method \Drupal\views\Plugin\views\ViewsPluginInterface::postSaveView() with a default empty implementation in \Drupal\views\Plugin\views\PluginBase.
Data model changes
None.
Original report by @merlinofchaos
Whenever you save a view, it rebuilds the menu, even if it does not.
Something I learned in Page Manager is that you can avoid rebuilding the menu a lot of the time.
The page display should be smart enough to be able to tell if the path(s) have changed. If paths or menu information has not changed, or it has no page displays at all, then menu should not be rebuilt. This can drastically speed up view saving in these cases.
| Comment | File | Size | Author |
|---|
Issue fork drupal-941970
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #1
dawehnerThis has to be discussed a bit
* Should cache plugins be able to hook into this process? They might store the caches somewhere.
* The block display has to clear the block caches, too.
Comment #2
dave reidShould probably be parent::delete()?
Rather than calling a menu rebuild, it's preferred to use variable_set('menu_rebuild_needed', TRUE); which will automatically rebuild the menu on the next request.
Comment #3
dawehnerOMG you suck!
Comment #4
iamEAP commentedJust chiming in to note the severity of the issue. Every time we save a view, our website comes to a grinding halt. Watching the DB live indicated that it was hits to menu/cache related tables.
Comment #5
incursio commentedWe are seeing the same thing in D7 ... views take forever to save, web site grinds to a halt. Looking at the semaphore table reveals that the menu_rebuild keeps firing .... sigh.
Comment #6
dawehner@incursio
The real solution is not to edit views on the actual page, but using exported views or some other proper deployment functionality.
Comment #7
incursio commentedThis *is* on a development machine.
Comment #8
jason.fisher commentedincursio, I temporarily resolve my problems by commenting out the menu rebuild/all cache flushing on view save and instead trigger those manually. I believe views may be triggering menu_rebuild multiple times: #1885668: Slow saving of views and site lockups
Comment #9
andrewbelcher commentedHere is a start at something for 7.x-3.x.
I have added a parameter to
views_invalidate_cache()to allow bypassing of rebuilding the menu:I have defaulted it to
TRUEso that other modules calling it continue to get the same behaviour.I've then updated
view::save()andview::delete()to no rebuild the menu and pass on to the displays to be do any additional cache invalidation with a new method inviews_plugin_display:Displays can override this to do things such as marking the menu for rebuild in
views_plugin_display_pageThere are a whole load of ways this could be better, but hopefully it's a start:
views_plugin_display_pageonly rebuild the menu when the path has changedviews_invalidate_cache()can only rebuild the menu when necessaryComment #11
andrewbelcher commentedLets try that again...
Comment #13
damiankloip commentedComment #14
damiankloip commentedPatch from over in #2226541: views_invalidate_cache() clears too much - closed as a dupe of this.
Comment #15
dawehnerJust in general, try to get inspired by the previous patches as well.
I don't think we should trigger a menu rebuild in the UI. Instead we should try to figure that out while saving / updating / importing the view.
let's describe what the $key is doing.
Let's ship with this.
Comment #16
damiankloip commentedFair, that was just a rough patch though, as you can probably tell.
Comment #18
damiankloip commentedComment #19
wim leersIs this still a problem ever since the menu homestretch patch landed?
Comment #20
moshe weitzman commentedviews_invalidate_cache() still rebuilds the router regardless of whether the current View even has a route (i.e. a page display). It also clears block cache regardless of whether the block display plugin is involved. If we think those are separate issues, we could reopen #2226541: views_invalidate_cache() clears too much
Comment #21
wim leersBerdir has confirmed that this is quite a big performance problem in #2241377-28: [meta] Profile/rationalise cache tags.
Comment #22
fabianx commented#8: This is I think strongly related to #2425259: Router rebuild lock_wait() condition can result in rebuild later in the request (race condition), which can also trigger rebuild again and again.
All thats needed is something doing a menu_get_item() while the cache is being rebuild ...
Comment #23
berdirI don't think this issue is about that problem.
This is about the following:
a) Assuming you have a view that only has a block, then saving that view will still trigger a router rebuild.
b) Assuming you have a view with a page display, but you are not changing any option that would require a menu rebuild, it will still do that.
(menu is somewhat misleading here and 7.x terminology)
Fabianx's issue is more related to this, as is #356399: Optimize the route rebuilding process to rebuild on write I think.
Also, there are two distinct problems with that I reported I think.
a) Multiple router rebuilds, which I've noticed in some cases, e.g. when saving views, but I don't think it's the fault of views.
b) menu cache tag invalidations, which did not happen during router rebuilds but at other times.. possibly saving menu links.
Comment #24
dawehnerJust some ideas.
Comment #26
dawehnerComment #28
dawehnerAnother try.
Comment #30
dawehnerMeh.
Comment #32
dawehnerLet's try.
Comment #34
dawehnerThis could fix a couple of the failures.
Comment #35
moshe weitzman commented"Invalidate the views cache, forcing a rebuild on the next grab of table data."
I'm not clear what we mean by 'table data'. I think we mean the views config entities themselves.
Comment #37
dawehnerThere we go.
Comment #40
berdirNice, this looks very promising!
Looking forward to views savings not taking forever when you just want to make a tiny change somewhere in a field handler.
Why use getContainer() here?
can we call resetCache() on the service here instead? That can also reset static caches.
Comment #41
wim leers+1 :)
The long time it takes to save a view (in D7 & D8) has always puzzled me. Sounds like this will make that go away :)
Comment #42
dawehnerThank you @berdir for the review.
Some work on it.
Comment #43
sharique commentedDo you mean array()?
Comment #45
dawehner@Sharique.
Well, I mean []. In PHP 5.4 you can use [] as alternative to array(), see http://php.net/manual/de/migration54.new-features.php
Comment #46
sharique commentedThanks dawehner, I did not know about that.
Comment #47
dawehnerSome work here.
Comment #49
dawehnerLet's see whether we managed to find all the failures.
Comment #51
dawehnerLet's see whether this is all we need.
Comment #56
mortona2k commentedReviewed with paboden.
Comment #57
mortona2k commentedI triaged with @paboden at Drupalcon Los Angeles Sprints.
The issue summary is understandable and up to date.
To reproduce, I set a breakpoint on function setRebuildNeeded() in RouteBuilder.php and saved a view with no changes. setRebuildNeeded() was called during the save, indicating an unnecessary router rebuild.
The last patch only had warnings during the last simpletest run. We might only need a reroll.
In postSave(), there is a comment before views_invalidate_cache() is called:
// @todo Remove if views implements a view_builder controller.
But does the router rebuild still cause crippling performance? If not, maybe this is no longer a major bug.
Comment #58
mortona2k commentedRerolled.
Comment #59
mortona2k commentedComment #62
martin107 commentedComment #63
fabianx commented#57: Generally we should try to avoid unnecessary work.
Comment #64
mpdonadioReroll. Didn't look to see b/c of which commits.
Simple conflicts in the use sections and where new code went.
Comment #66
mpdonadioI can't get Drupal\config\Tests\ConfigImportAllTest to complete locally to see what is really going on.
Comment #67
lendudeReroll, didn't apply anymore. Didn't change anything else except remove some double newlines.
Comment #69
joelpittetJust a guess:
Do we need to call
$executable->initDisplay();before the foreach?Comment #70
lendude@joelpittet sounds like a good guess, lets see what it does. Like @mpdonadio I can't get Drupal\config\Tests\ConfigImportAllTest to pass cleanly locally, but fails with timeouts and stuff so seems unrelated to this.
Added missing use and cleaned up a white space error that snuck in.
Comment #71
dawehnerGiven that StylePluginBase has no interface it would be at least save to add it to there, so we don't need a methodExists() check. Ideally though we would have something like
ViewChangesInterfacewhich would contain all that kind of methods.... it would be really helpful to explain what we actually check in that 12 lines of code.
Comment #72
lendude@dawehner something like this?
Comment #73
dawehner..., because the formats are stored on the route.
Do you mind documenting that the original display is empty when we just created a new view?
Comment #74
dawehnerComment #75
lendudeUpdated per #73
Comment #76
dawehnerThis sounds something which should live in
DisplayPluginBaseComment #77
lendude@dawehner yeah that makes sense. Rerolled for #76 and since #2464657: Remove unnecessary cache clear in Views tests landed, that bit can be taken out too.
Comment #78
dawehnerSo we are adding this to DisplayPluginBase but we don't add it to StylePluginBase, but still call the method?
Comment #79
lendude@dawehner We don't? Or are you referring to something else? If so, could you elaborate a bit? Patch didn't apply to 8.1.x and 8.2.x so rerolled for that, #77 still applies to 8.0.x.
Comment #80
dawehner@Lendude No idea what I was smoking on sunday :)
I'm wondering whether we really want to do this here ... isn't it a sign of some brokenness if you call out to a display that doesn't exist? This implementation here also doesn't care about the reference properly. You would rather setup
$this->display[$display_id]instead.Comment #81
lendude@dawehner haha! No worries. At first glance I'd say I agree with you that that bit looks bad/weird, I'll take a look at it when I get some time.
Comment #82
lendude@dawehner so do you mean something like this?
Comment #83
dawehnerWe add a page display by default? This feels weird
Comment #84
lendudeThis is the default in ViewEntityInterface
and since we want to set $id, I figured using the default for $plugin_id for that method made sense. Unless there is a 'Broken' display, but don't know if there is anything like that.
Comment #85
dawehnerFair point. In general i'm still wondering why this issue requires us to adapt getDisplay in the first place.
Comment #86
lendudeYeah I was wondering the same thing, it seems unrelated. It first showed up in your patch in #42 so I assumed it was causing some tests to fail.
Comment #87
dawehner@Lendude
Yeah that sounds like a workaround for some missing display.
Comment #88
lendudeOk so lets see what still fails, and why, when we take it out.
Comment #89
dawehnerI think we should also detect whether we got new arguments ... as this will change the required route as well
Comment #90
lendude@dawehner Isn't that covered in PathPluginBase::postSaveView()? Is there ever a need to rebuild routes when not using/extending PathPluginBase?
Comment #91
dawehnerYeah not sure, I was probably just blind.
Comment #92
alexpottWe've entered the rc phase for 8.1.x and this is an API addition therefore moving to 8.2.x.
Comment #93
damiankloip commentedThe module handler and block manager can be injected instead now?
This could be quite important that it happens, so I think maybe the style plugins should be handled in View::postSave() too. Rather than relying on the parent method being called.
Thank you! nice.
Doesn't seem any point in setting a variable for the module handler here. Can just be a one liner.
Maybe we should be deprecating this now too, with a note that code should just use Views::invalidateCache()
Comment #94
lendudeThanks for the feedback @damiankloip! So, something like this then?
Comment #95
dawehnerSo what about supporting all plugin types?
Comment #96
lendude@dawehner so something like this? Moved postSaveView to Drupal\views\Plugin\views\PluginBase and then call it for all plugins that are present in each display.
Comment #97
xjmThanks @paboden and @mortona2k triaging this issue last year, and thanks @Lendude for helping the maintainers with recent work on the issue.
@tim.plunkett, @dawehner, @alexpott, and I discussed this issue at DrupalCon New Orleans and decided to defer deciding whether it should remain major for two reasons:
Comment #98
dawehnerGiven we need profiling, let's move it to needs work.
Comment #110
catchMenu rebuilds used to take several seconds but we've done a lot to optimise that since 2010:
1. Router rebuild and link discovery are separated, so that link discovery doesn't block other request routing (and sometimes doesn't happen at all).
2. setRebuildNeeded() means that for configuration deploys instead of every views save triggering a rebuild, there should only be one.
I think that means it's time to downgrade this from bug to task. However from what I can see the approach would still help a couple of cases:
1. Speed up saving of views for admins/site builders (when they're likely to be the person triggering the next request that rebuilds the menu).
2. Optimise away that last unnecessary rebuild on config deploys even if it's just 1 instead of views saves * 1.
Comment #115
claudiu.cristeaAs we deprecate the procedural code, I'll, make this part of the #3566536: [meta] eliminate core .module files effort. Will try to revive it
Comment #116
claudiu.cristeaComment #117
claudiu.cristeaHide patches
Comment #119
claudiu.cristeaFixed IS
Comment #120
claudiu.cristeaComment #121
claudiu.cristeaReady for review
Comment #122
claudiu.cristeaUnassigning
Comment #123
nicxvan commentedWent through this a few times, I think this makes a lot of sense now. It's separating the cache invalidation and the route rebuild so each display type can handle the part they need to.
Since this isn't a straight replacement I think we should do a CR here and reference that one.
Comment #124
nicxvan commentedI created a first pass at the CR and suggestions to swap it. If the CR looks good we can apply those and update https://www.drupal.org/node/3566774 to remove the reference to this function.
Once that is done I think this is ready!
Comment #125
smustgrave commentedSmall question on the MR.
Comment #126
nicxvan commentedJust answered!
Comment #127
nicxvan commentedThank you for cleaning up the comment and block plugin change, I think this is ready!
Comment #128
alexpottAdded some comments to the MR.
Comment #129
claudiu.cristeaReady for a new review
Comment #130
nicxvan commentedAll comments have been addressed, looks good to me!
Comment #131
alexpottAdded some more thoughts to the MR. Not entirely sure about the answers - needs discussion.
Comment #132
nicxvan commentedThe de duplication seems like a good idea.
Comment #133
graber commentedI'll add my few cents here.. This branch creates new API for basically all views plugins in the form of postSaveView(). Since it'll be always there, possibly contrib will use it now so we have no way of predicting what will happen there. Deduping is a bit dangerous as it may happen some callbacks will actually need to be called multiple times, just with different arguments or even same arguments, we have no idea how will people use the new API. I'd pass some initially empty $context array by reference in each call to postSaveView() and handle possible duplication on the plugin level.
BTW.. Seems also View::routeRebuildNeeded() can be called multiple times, even if harmless, it's not needed so if the $context solution will be the preferred one, calling View::routeRebuildNeeded() only once should be included on the plugin level too. I'd move that method to core/modules/views/src/Plugin/views/PluginBase.php probably (protected).
Comment #134
graber commentedAbout the base plugin class method
postSaveView()concern:Problem:
Views has two plugin families on one base class:
HandlerBase extends PluginBase implements ViewsPluginInterface, so handlers (field,filter,sort, …) are plugins — butgetPlugin()/getAllPlugins()only cover'plugin'types, while handlers are reached via the separategetHandlers()/getAllHandlers(). SopostSaveView(), added toViewsPluginInterface+PluginBase, is inherited by handlers thatView::postSave()never iterates (it only walksgetPlugin()), andBlock::postSaveView()clears the block cache once per block display (N times).Proposed solution:
Move
postSaveView()to a dedicated opt-inPostSaveViewInterface(with a by-ref$context); remove it fromViewsPluginInterfaceandPluginBase.Implementers (
PathPluginBase,Block, RESTSerializer) still perform their own effect, but record it in$contextso a peer that would repeat a view-global effect skips it.View::postSave()iterates display handler +getAllPlugins(TRUE)+getAllHandlers(TRUE)(same coverage ascalculateDependencies()), dispatches toinstanceof PostSaveViewInterface, and stays generic — it seeds$contextbut never acts on it, as in my last comment - plugins use it to avoid duplication.Why:
Performance: block cache cleared / routes rebuilt once per save, no static state.
Controlled API: opt-in interface instead of forcing the method onto all ~19 types via
PluginBase.Complete coverage: handlers can now participate, and
$only_overrides = TRUEvisits each configured plugin/handler once, not once per inheriting display.Comment #135
graber commentedSince there's no more discussion here, setting this to NW.