Problem/Motivation
After #3377570: Add PHP Fibers support to BigPipe BigPipe will support PHP Fibers for JavaScript placeholder rendering. Once we add #3259709: Create the database driver for MySQLi and views support for that, rendering can happen while waiting for async queries to return etc., and parts of the page will render faster.
However, the same approach is (equally?) valid for non-BigPipe placeholder rendering too.
For a simplified example, let's take four blocks, all of which contain #lazy_builder placeholdered content.
Three blocks are views with a slow listing query, let's say 3 seconds per query.
One block is an http request to a web service, also takes 3 seconds (slow web service).
If these placeholders are rendered in sequence 3 * 4 = 12 seconds of blocking i/o.
If we implement async query and http request support + fibers, then 3 * (4/4) = 3 in the best case - i.e. the now non-blocking i/o calls all get sent within milliseconds of each other.
Steps to reproduce
Proposed resolution
Remaining tasks
User interface changes
API changes
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|---|---|---|
| #16 | 3383449-16.patch | 5.68 KB | catch |
| #16 | interdiff.txt | 1.52 KB | catch |
| #12 | 3383449-12.patch | 5.22 KB | catch |
| #11 | interdiff.txt | 2.26 KB | catch |
| #9 | 3383449-9.patch | 4.29 KB | catch |
Issue fork drupal-3383449
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 #2
catchI have no idea if this works, just ran linting and one umami test, but something like this.
Comment #3
catchShould be better.
Comment #4
andypostThis looks like pattern so "fiber's loop" could be extracted to some trait or static method somehow
Comment #5
kim.pepperWow! It feels like a whole new world of concurrency.
Can we add typehints?
Comment #6
catchPotentially yes but I'm not sure how - half the foreach loop is the same as the other patch, but half is doing fairly arbitrary things with arbitrary variables.
We certainly can.
Also added $fiber->suspend() to an existing lazy_builder callback in the renderer unit tests.
Comment #7
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #8
catchCS clean-up.
Comment #9
catchRendererInterface lies - renderPlain returns Markup or string.
Comment #10
wim leersNice! 🤩
\Drupal\Core\Render\Renderer::renderPlaceholder()does 3 things:I'm kinda worried about the new public
doRenderPlaceholder()which does parts of the first and the second. The 3 different aspects are sprinkled throughout various parts: some in fiber creation, some in the new public method, some in the fiber processing. I'm hoping we can tighten that up a bit? (It took me a good while to grok what's going on because of this 🙈)Comment #11
catchWe can drop the helper and move it back inline - I added the helper to share code between renderPlaceholder and the fibers code, but it breaks the rule of 3 and is more code and indirection.
To get rid of the duplication we'd need to render the messages placeholder as a fiber, and in the process ::renderPlaceholder() would become unused, although still required by the interface. I don't know how to reconcile that bit.
Just posting the interdiff for discussion rather than a new patch for now.
Comment #12
catchActually might have a better answer to #10, now renderPlaceholder() is three lines of code, with two protected helpers.
Comment #13
wim leersI like #12 much better! 🤩 Much easier to understand, because the original logic flow can still easily be seen:
public renderPlaceholder()now just calls 2 other things (doRender()+doReplace()) → public API surface unchanged 👍protected replacePlaceholders()calls those 2 same things directly instead of callingrenderPlaceholder(), to give it more control 👍The most important caller of
public renderPlaceholder()is BigPipe. This is fine, because BigPipe has its own parallellization.Why both?
An obvious question is: do we need both?
This is my analysis/understanding, could you confirm its correctness, @catch? 🤓🙏
\Drupal\Core\Render\Placeholder\SingleFlushStrategyfaster, by rendering the different placeholders in parallel (in the sense that it's blocking I/O can happen in parallel), but still remains a single flush. Placeholders can be rendered in arbitrary order, but they must ALL be rendered forRenderer::renderPlaceholders()to finish (and of course, messages last).\Drupal\big_pipe\Render\Placeholder\BigPipeStrategyfaster, by rendering in parallel (same limitations as above) and streaming the fastest to render placeholder (least I/O-blocked) to the client first (but still guaranteeing that the messages placeholder will be rendered & streamed last).The two issues are complementary: this issue makes all placeholders renderable in fibers and provides performance benefits (both back end and front end) even when BigPipe is disabled, whereas the other issue makes it possible to not have to tweak the placeholder render order: whichever one is least blocked on I/O will be served fastest (also both back end and front end, but only more front end performance benefits: just the same back end performance gain).
Comment #14
catchOK good that you like #12, I think it's the cleanest we can do. The split is necessary so we can have independent return values without trying to pass the same array to different fibers by reference, which I do not even want to find out if it works because it would be impossible to grasp even if it somehow does.
#13 is correct, I also opened #3383521: Document why BigPipe no-js placeholder rendering doesn't use Fibers but to me that currently looks like the most work/least return (because we would render placeholders out of order, but still need to flush them in order, vs replacing as they come back as with bigpipe or waiting until they're all done as here). However we could do that and then it would cover every possible way that placeholders get replaced in core unless I've missed one.
And yes they're 100% complementary with no inter-dependencies. It all relies on #3259709: Create the database driver for MySQLi and #3379883: [PP-1] Add async query execution support to views (and perhaps #3257725: Add a cache prewarm API) to get the actual performance benefit.
This is the amazing thing with fibers for me, the complete separation between 'I want to execute some code in a fiber because I potentially can run some other code if it suspends' and 'if I'm executing in a fiber, I'll suspend while some non-blocking i/o is happening to allow something else to happen' means we can add support in completely different places without conflicts or introducing interdependencies. Then it fits perfectly into Drupal's render API where at the very highest rendering level we end up with a set of placeholders which could have been nested deep inside entity/field/block/layout rendering pipelines, but end up getting processed all at once.
Note I also noticed #3383488: Layout builder should use #lazy_builder to render blocks while looking at this stuff, which would benefit dynamic_page_cache and big_pipe with or without Fibers.
Comment #15
wim leers😱🤣 Yeah let's not!
Yeah I personally would lean to not doing that issue — or rather, change the scope documenting why it's not worth it 😇
💯💯💯💯 🥳
All that hard work you, I, @FabianX and a few others did at the end of the Drupal 8 cycle is finally reaching its full potential 😊🤝
Comment #16
catchDiscussed this and related patches with KingDutch today - mostly in the context of comparing the pure-fibers implementation here with libraries like amphp/revoltphp. That particular bit we are still discussing, there are features those libraries provide, but they are (so far) a few lines of code with pure-fibers, or harder to adopt (like amphp's async MySQL connection pool which would impose using a different database API to core's when you want to use it, or a fair bit of subclassing/decorating).
But we both realised one improvement can be made here on top of the current implementation.
Let's say we have a situation where four placeholders have executed four async queries and none have returned yet, so we're into our second iteration in the loop, we can end up in a situation where there's nothing else to do except 'spinlock' - going around the loop until one returns.
We can't perfectly detect that case, but we can start looking for other things to do in the case we get into the second iteration and things are still suspending. In this case, the renderer code can check if itself is in a fiber, suspend that fiber, and this would fall back to #3257725: Add a cache prewarm API in DrupalKernel::handle() (or something in-between if there was something) - widening the pool of things we can do while we wait.
See interdiff and patch.
This pattern can also be applied to the BigPipe implementation although I've not done that yet.
Comment #18
catchComment #19
smustgrave commentedWasn't entirely sure how to test.
Did the same thing as #3377570: Add PHP Fibers support to BigPipe and applied the patch
Verified forms and messages still render just fine.
Since #3377570: Add PHP Fibers support to BigPipe is RTBC going to RTBC this too!
Comment #21
catchRandom test failure.
Comment #22
alexpottCommitted d41f1a0 and pushed to 11.x. Thanks!
Comment #26
reinfate commentedFound that this has the same issue as described here https://www.drupal.org/project/drupal/issues/3377570#comment-15271179
The fiber loop itself is never suspended due to $iterations being reset at every iteration of the while loop. Changed that in MR!5003
Comment #27
smustgrave commented@ReINFaTe can you please open a follow up.
Comment #29
bobburns commentedI had to change line 184 to
// Render the placeholder into markup.
$markup = $this->renderPlain($placeholder_element);
// return $markup;
return $markup ? $markup : '';
}
or it throws a "null given" error