Problem/Motivation
After #2807501: ResourceResponse::$responseData is serialized, but is never used: unserializing it on every Page Cache hit is a huge performance penalty was committed - which hugely improved performance for anonymous responses (thanks to page_cache.module), I realized that I probably should look at the REST module's performance for authenticated responses too (by ensuring it's accelerated properly by dynamic_page_cache.module).
Turns out it is indeed not effectively accelerating things. In fact, it suffers from the same problem that #2807501, but for different reasons.
Proposed resolution
ResourceResponseSubscriber reacts to the KernelEvents::RESPONSE event, with priority 5 (to run before FinishResponseSubscriber). This means it runs after DynamicPageCacheSubscriber::onResponse() (with priority 100) … which means Dynamic Page Cache is serializing the ResourceResponse, where $responseData still contains an entity to unserialize (the response has not yet been "flattened" by \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::flattenResponse()). And hence it's fairly slow: because even in case of a Dynamic Page Cache hit, the entity must be PHP-unserialized and then serialized into the requested format: JSON, HAL+JSON, or something else.
Fortunately the solution is therefore quite simple: further increase the priority of ResourceResponseSubscriber: from 5 to >100 (because DynamicPageCacheSubscriber has priority 100).
We could choose to go a bit further, and not only make ResourceResponseSubscriber run before DynamicPageCacheSubscriber, but also make it lighter: don't inject any dependencies, and instead request them from the container at runtime (so that in case of a Dynamic Page Cache hit, we did not incur the cost of instantiating those services). But #10 has shown that the incremental performance gain of that is minimal (lower than the standard deviation even), and therefore not worth the additional complexity. Besides, that could still be done in a follow-up. This issue is about fixing the glaring performance problem first.
The end result is a ~15% performance boost: from 76 ms/req to 65 ms/req for requesting a serialized JSON article node. The difference will be bigger with more complex content (e.g. many fields, lots of data in the fields). This is measured on a PHP 7.0.12 setup.
Remaining tasks
None.
User interface changes
None.
API changes
None.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #49 | interdiff-46-49.txt | 2.34 KB | effulgentsia |
| #49 | 2827797-49.patch | 5.45 KB | effulgentsia |
| #46 | 2827797-46.patch | 5.48 KB | wim leers |
| #39 | 2827797-39-tests-only-FAIL.patch | 5.13 KB | wim leers |
| #38 | 2827797-38.patch | 6.38 KB | wim leers |
Comments
Comment #2
wim leersComment #3
wim leersComment #4
wim leersAttached: the patch that includes the steps towards a solution that I attempted in the IS. For future reference.
Comment #6
wim leersThis needed a reroll: #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method changed the priority of
ResourceResponseSubscriberfrom 0 to 5, because it must run beforeFinishResponseSubscriber.Comment #8
wim leersLet's get this issue going again.
Comment #9
wim leersSo we have 3 states:
DynamicPageCacheSubscriber.DynamicPageCacheSubscriberand stop injecting its dependencies,instead retrieve them at runtime, when we know it's going to be called.
We know we want to move from state 1 to either state 2 or state 3. State 1 is incorrect. States 2 and 3 are both correct.
The sole remaining question is: do we want to go to state 2 or state 3? State 3 has potentially better performance, but comes with ugliness.
We can answer this question by doing the necessary profiling. So, working on that.
Comment #10
wim leersPerformance of the 3 possible states
Analysis
Going from state 1 to state 2 yields a significant performance boost: 15% faster responses (11 ms less per request). Standard deviation unchanged. A difference of 3.5 standard deviations.
Going from state 2 to state 3 yields a negligible performance boost: 1.5% faster responses (1 ms less per request). Standard deviation slightly lower (probably because less code is executed on cache hit requests). But the performance gain is less than half of the standard deviation of state 2/3.
Conclusion
State 3 is not worth the additional complexity. State 2 yields most of the gains. The reason we don't see bigger gains by going from state 2 to state 3, is that the bootstrapping + routing + access checking represent the vast majority of those 65 ms to generate a response, hence we only observe a 1.5% (1 ms) additional performance gain.
Comment #11
wim leersWe need to update the IS to reflect the findings of #10. But first, we need test coverage to prove that in HEAD, Dynamic Page Cache is not accelerating REST responses at all.
IOW: we want a failing test-only patch, and then a passing tests+fix patch. Working on that.
Comment #12
dawehnerResourceResponseSubscriberthis let me thinking ... isnt' that what\Symfony\Component\HttpKernel\KernelEvents::VIEWis designed to do?Could this convert the response over?
Comment #13
wim leersBefore posting a failing test, here's a test showing that in fact Dynamic Page Cache was already working. Like the issue summary says:
i.e. Dynamic Page Cache is caching a
ResourceResponsethat still contains a\Drupal\node\Entity\Nodeobject. So, we need to read that from cache, turn it into a PHP object (un-PHP-serialize it), and then serialize it down to JSON (or HAL+JSON or whatever the requested format is). What we should be doing, is serialize thatNodeobject to JSON (or HAL+JSON or …) and then let Dynamic Page Cache cache it.That is what this issue is fixing.
Next: a failing test.
Comment #15
wim leersThe reason #13 failed for
MenuLinkContentandShortcutis that their canonical URLs are admin routes because their URL path starts with/admin/…. Dynamic Page Cache explicitly avoids caching admin routes.(We could change that in Dynamic Page Cache for REST routes that happen to start with, but that's out of scope here.)
Comment #16
wim leersOr we could let the REST module undo what
\Drupal\system\EventSubscriber\AdminRouteSubscriberdoes, but only for REST routes.That'd be much more elegant than what #15 does. All of the complexity in #15's interdiff would go away!
And it'd be within scope because we're only touching
rest.module.(Of course, ideally,
AdminRouteSubscriberwould only be touching_format: htmlroutes! This again shows that Drupal 8 is not really API-first yet: many things are still HTML-centric.)Comment #17
wim leersNow, let's finally add the failing test coverage, to prove that this functionality has poor performance in HEAD.
Comment #18
wim leersAnd then finally, let's bring the fix ("state 2", as discussed in #10).
#17 should be red, this should be green :)
Comment #19
wim leersAnd now, finally, the IS has been updated.
This is definitely ready for review, and I think/hope an RTBC is within reach :)
Comment #22
tedbowIs there any there anything we can check for besides
strpos($name, 'rest.') === 0? I guess modules are suppose to use the machine name for module as the start to all their routes?What if we ever wanted to add an non-resource route to the REST module. Like some sort of config form. What if a custom module did this and didn't follow the rule about machine names?
What about
$route->getDefault('_controller') == 'Drupal\rest\RequestHandler::handle'? I wish there was a better way we could tell if a route was REST resource.Think this is close to RTBC. I TEST_ONLY patch would be good prove the ResourceResponse object is stored currently in the Dynamic Page Cache.
Comment #23
dawehnerRight, but what is the reason we cannot do that? Can't we check whether format is set, and in case it doesn't support HTML we would not set the option ...
Comment #24
wim leers#22: it's best practice that routes prefix their route names by the module name. I've yet to encounter routes that use another module's route name as the prefix, which is the only way this could cause problems for something else. The test-only patch is the patch in #17.
#23: Yes, we could. I'd prefer that, actually. So let's see if we can do that: done, let's see if all tests still pass.
Comment #25
wim leersGlad to see it's passing :)
Comment #26
tedbowMy point in #22 is no longer a concern because #24 removed RestRouteSubscriber.
The patch looks good to me but will let @dawehner RTBC if thinks it is good.
Comment #27
dawehnerI agree for now just changing the priority is the right thing to do. Moving to the view event would be certainly way more of an effort.
Nice test coverage
I was about to say: it'd be nice to have test coverage, but you know, here is an updated patch with it.
Comment #29
dawehnerIt works on my system :P
Comment #30
wim leers#27:
Sadly, PHPLint is failing… because PHP 5. Your code works fine in PHP 7. Easy to fix. EDIT: dammit, you already did that in #29 :)
I think @tedbow can now RTBC this one :)
Comment #31
tedbowNew test coverage for AdminRouteSubscriber looks good.
Woot! Thanks @Wim Leers and @dawehner for your work on this! RTBC
Comment #32
wim leersAs of #2765959-182: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage, this now blocks #2765959.
Comment #33
wim leersComment #34
effulgentsia commentedCan this be moved to a separate issue (even if it means postponing this one on that). I'd like it to have a dedicated commit in the commit history and a dedicated change record. Because this will introduce some (potentially desirable) side effects. For example, it will restrict
AdminPathConfigEntityConverterandLanguageNegotiationUserAdminto only operate on HTML routes. As well as similar code in contrib. It's not entirely obvious to me whether for those 2 examples if that's a desirable change or not: is the concept of "administration" and changing config overrides behavior and/or language negotiation based on whether you're "administering" a strictly HTML concept?Comment #35
effulgentsia commentedIf the concept of non-HTML "administration" is still something that's meaningful to preserve, then the CR for the hunk in #34 could just indicate that the behavior of automatically registering routes with
/admin/*paths as administrative routes is limited to only HTML routes. And that if someone wants a non-HTML route to be administrative, then they need to explicitly add the_admin_routeoption to the route themselves, regardless of that route's path. However, that kind of begs the question of why we want to make/admin/*paths have different defaults based on the format.Maybe that's a bug with those 2 entity classes? Does it make any sense that their HTML routes are administrative? Furthermore, the
MenuLinkContentone ends in/edit, which is maybe also a bug?Comment #36
wim leersThat's … a great point!
So let's go back to the patch in #18 then. I don't think we should block this on deciding how to change the semantics of the
_admin_routeroute option, because clearly it has unintended consequences that need further investigation.Comment #37
dawehnerThis documentation just tells what is happening, but it doesn't tell why this is done.
Comment #38
wim leersDiscussed this with Daniel at DrupalCon Baltimore — we both independently realized that #36 (i.e. the approach in #18) does not actually address the concerns raised by @effulgentsia in #34 + #35.
Because the end result is still that
AdminPathConfigEntityConverterandLanguageNegotiationUserAdminwill only operate on HTML routes.So, we realized there's a simpler solution: update
\Drupal\dynamic_page_cache\PageCache\ResponsePolicy\DenyAdminRoutesto only ignore HTML admin routes. Then we avoid all of these negative consequences.(We still should make the concept of "admin routes" sane: it's currently too broadly defined, and hence causes consequences like this one. But solving that is a massively complicated "unravel lots of things" task which this issue should not be blocked on.)
Therefore I present this simpler approach :) (Which still needs tests.)
Comment #39
wim leersAnd tests. (The test-only/FAIL patch is also the interdiff.)
Comment #40
wim leersAnd I think this issue definitely qualifies as major to ensure we don't regress in this (i.e. don't break BC).
Comment #42
effulgentsia commentedIf the reason this policy rule exists is for the reason stated in the above PHPDoc, then I don't see why it makes sense to couple it to the HTML format. If we don't want to cache low hit ratio HTML responses, then why do we want to cache low hit ratio non-HTML responses? And if a particular resource has a low hit ratio in HTML, then why would it have a high hit ratio in non-HTML? Maybe there are logical answers to that, but if so, they need to be documented.
However, I don't think the problem is with this policy, but rather with the entity types that define incorrect canonical link templates. Therefore, I opened #2873529: BlockContent, MenuLinkContent, and Shortcut define incorrect canonical links. If we think that analysis is correct, but don't want to hold up this issue on resolving that one, then perhaps it makes sense to meanwhile write this patch's tests to not expect caching for the entity types with canonical resources at admin routes?
Comment #43
wim leersWho says these are low cache hit ratio? For example
Shortcut(/admin/config/user-interface/shortcut/link/{shortcut}) andMenuLinkContent(/admin/structure/menu/item/{menu_link_content}/edit) are content entities with an/adminpath prefix that clearly are frequently used.The PHPDoc you quoted is specifically for the admin area of a Drupal site, i.e. where site builders go to configure things. Clearly, retrieving entities via REST is something different entirely. We just have to adjust
DenyAdminRoutesto do what it actually intended to do, and the reason that it wasn't already doing that, is exactly that_admin_route's semantics are poorly defined.I see what you are saying, and that would be nice, but … then
DenyAdminRouteswould still be insufficiently precise/strict (again, due to_admin_route's vagueness), and therefore any contrib or custom entity types whose routes use an/adminpath prefix would suffer from the same problems.Therefore I think we should indeed have the discussion that you started in #2873529: BlockContent, MenuLinkContent, and Shortcut define incorrect canonical links, but I also think we should do what's in #39.
Comment #44
effulgentsia commentedI agree that REST GET requests for entities don't make sense to treat as administrative routes. A way to express that would be to change
EntityResource::getBaseRoute()to add the'_admin_route' => FALSEroute option. I don't know whether that should be just for GET methods or if it should be added to all methods. I also don't know if it should be broadened to all REST resources by default, not just entities. If so, then that could be moved toResourceBase::getBaseRoute(). I suggest opening a separate issue to figure out the scope we want for that.I also think that if we want to change
DenyAdminRoutesto be scoped to just HTML routes, we should do it in a separate issue. But I'm not convinced of the logic for doing so, other than to work around the bug that some routes are incorrectly marked as administrative.I still think that the scope of this issue should be to change our tests to not expect caching for the problematic entity types. And then change those tests again when one of the other issues is correspondingly fixed.
Comment #45
dawehnerI think I agree, mostly because caching of those is less important than most of content entities, which are on not admin routes. So even if we don't support caching for those, we still have a major benefit.
Comment #46
wim leersOkay then…
Done: #2874938: AdminRouteSubscriber must only mark HTML routes as administrative. I posted my concerns about this in that issue.
Well… that's exactly the sort of thing that request & response policies exist for. But anyway, clearly that's not going to be accepted.
So, this brings back much of what #15's interdiff was doing. Which is to say: very complex work-arounds for what is otherwise trivially simple. At least this means that once we fix the underlying problems, the test coverage becomes simple once again.
Comment #48
wim leersPasses fine locally.
And sure enough:
PHP segfault; most likely random.
Comment #49
effulgentsia commentedI opened #2877528: Change Dynamic Page Cache's response policy from "deny admin routes" to "deny html admin routes", and changed the @todos in this patch to reference that issue in addition to #2874938: AdminRouteSubscriber must only mark HTML routes as administrative.
As a side-note, #2877136: dynamic_page_cache.module has an incorrect and misleading @file doc tripped me up while digging into all that.
As this is only a comment change, leaving at RTBC. I'll commit this tomorrow unless objections are raised before then.
Comment #50
effulgentsia commentedTicking the credit box for @tedbow for his reviews.
Comment #51
effulgentsia commentedDo we need a CR for this? Normally, I don't think changing the priority of a subscriber automatically requires a CR, but I'm curious if any of you know of a reason to provide one in this case.
Comment #52
wim leers#49: I commented at #2874938-4: AdminRouteSubscriber must only mark HTML routes as administrative and #2877528-6: Change Dynamic Page Cache's response policy from "deny admin routes" to "deny html admin routes". I posted a patch at #2877136-2: dynamic_page_cache.module has an incorrect and misleading @file doc. Thanks for your thoroughness!
#51: No, I can't think of a reason.
Comment #54
effulgentsia commentedPushed to 8.4.x. #2807501: ResourceResponse::$responseData is serialized, but is never used: unserializing it on every Page Cache hit is a huge performance penalty was tagged for release notes mention, so tagging this one too.
Comment #55
effulgentsia commentedThis test seems a little brittle. If the cid pattern changes in the future, this test will return an empty array and the foreach will get silently skipped, so the test will still pass. Perhaps there should be a followup to improve on this? Do we really have a situation where we don't know the exact $cid to check for?
Comment #56
wim leersYAY! So glad to see this in! This unblocks #2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage, which I'm now rerolling.
#55: Well-spotted! Opened a follow-up for that, with patch: #2877778: Harden test EntityResource + Dynamic Page Cache test coverage.
Comment #58
wim leers