This is a place for me to formulate my thoughts on various things regarding Drupal 8 performance. Many of them based on discussions at Amsterdam.

Some of them are also not performance related.

--

Also has still just brain dump and needs actionable issues created:

#2294569: Determine cause of high memory consumption

Comments

Fabianx’s picture

Title: [Ignore] Fabianx' Drupal 8 performance master plan » [Braindump] Fabianx' Drupal 8 performance master plan

Better title ;)

Fabianx’s picture

RenderContext braindump:

08:22 Or TL;DR:
08:23 - Return out-of-band + render_array always
08:23 - If you need to return a string, merge with whatever is the current rendering context on the stack
08:23 merge into
08:23 so its preserved.
08:24 - If there is no rendering context => Exception
08:24 === done
08:25 If you need whatever comes below isolated from the rest, start a new render context via closure.
08:25 => early rendering possible and again encouraged :p
08:25 => drupal_add_js possible again :p

Problem/Motivation

Drupal 6 used drupal_add_js / drupal_add_css exclusively, that made caching difficult and there was one global that tracked it all.

theme() was rendered directly to a string and that was all there is.

=> One global state, tracked per page request

In Drupal 7 there was both drupal_add_js() / drupal_add_css() to track JS and CSS and #attached for js / css, but #attached was very much preferred.

=> State per render array, so it was cacheable and global state per page -> still treated as a big page

In Drupal 8 there is only #attached and global state was mostly removed, however because of twig rendering of render arrays also needs to preserve state and hence we introduced a stack.

=> State per render array, state per render recursion 'call stack'.

This stack is constructed on demand when its not yet there and that is a problem as it then for edge cases acts like an implicit global again.

The stack being merged back into the render arrays makes it also way simpler to rely on just #attached for render caching.

Proposed resolution

All of those have in common that there needs to be some way to track the state of the system, in D6 one global, in D7 globals and pass-around, in D8 Stack and pass-around.

The optimal solution would be to have drupal_render() return a Renderable of some kind: call it HtmlFragment, render array with #markup and #attached, value array [$markup, $attached], value object, etc.

This had never been feasible due to the BC break of drupal_render() no longer returning a rendered string.

However must it be changed?

No, the reason why a stack based approach works is that we only need to merge the render array at distinctive points:

a) when we have #cache 'keys' or #render_placeholder set in the render array
b) when we return from a Renderer::render() call (and hence not call recursively our own via Renderer::doRender())

The stack and all other things so far have been implicit. The idea is to now make it explicit and then opt-in several things to have a default 'Rendering context'.

- Whenever something is rendered, it needs to be rendered onto a rendering context.
- When something is recursively rendered (a new render() call while being in another render() call, it is rendered into a sub-context (new level) of the current rendering context and then merged back once we return from that render() call.

The functions would look like this in pseudo-code:


public function renderOnNewRenderContext($callable) {
  $old_render_context = $this->renderingContext;
  $this->renderingContext = new RenderContext();
  $this->renderingContext->pushFrame(new BubbleableMetadata());
  $markup = call_user_func($callable);
  $frame = $this->renderingContext->popFrame();
  $this->renderingContext = $old_render_context;

  $elements = [];
  $elements['#markup'] = $markup;
  $frame->applyTo($elements);

  return $elements;
}

public function render($elements) {
  if (!isset($this->renderingContext)) {
     throw new Exception("Need something to render on."); 
  }

  $markup = $this->doRender($elements);
  $frame = $renderingContext->popFrame();
  $frame->mergeWith($elements);
  $this->renderingContext->pushFrame($frame);

  return $markup;
}

protected function doRender() {
  if (!empty($elements['#cache']['keys'])) {
      $this->renderingContext->pushFrame(new BubbleableMetadata());
  }

  // render everything, etc. ...

  if (!empty($elements['#cache']['keys'])) {
    $frame = $this->renderingContext->popFrame();
    $frame->mergeWith($elements);
    $frame->applyTo($elements);
  }

}

Remaining tasks

User interface changes

API changes

=======

More braindump:

- hook_entity_view() uses drupal_add_js(), but needs to end-up within the same render array level => #pre_render pattern solves that, else Exception.

====

Of note is that unless something wants to #cache or placeholder something in the tree, it does not matter if in the end it all ends up merged within the same 'global'.

Even in D6 / D7 one global works as long as there is no other cache than the page cache!

Hence only #cache / #render_placeholder needs to start a new level => reduces complexity.

====

Even for a new context a new level would be sufficient.

Every level means completely isolated rendering and it is removed from the stack completely in the end.

=> Do we even need a stack, is the call stack not enough? => Likely problems with Exceptions, but then we destroy the stack, batch API, we return from the call stack ...

Lets see if this would work:


public function renderOnNewRenderContext($callable) {
  $old_render_context = $this->renderingContext;
  $this->renderingContext = new BubbleableMetadata();
  $markup = call_user_func($callable);
  $frame = $this->renderingContext;
  $this->renderingContext = $old_render_context;

  $elements = [];
  $elements['#markup'] = $markup;
  $frame->applyTo($elements);

  return $elements;
}

public function render(&$elements, $is_root_call = FALSE) {
  if (!isset($this->renderingContext)) {
     throw new Exception("Need something to render on."); 
  }

  $markup = $this->doRender($elements, $is_root_call);
  $this->renderingContext->mergeWith($elements);

  return $markup;
}

protected function doRender(&$elements, $is_root_call = FALSE) {

  // Render cache items within a new render context.
  // @todo move down to render_children instead.

  if (!empty($elements['#cache']['keys']) && !$is_root_call) {
     $renderer = $this;
     $elements = $this->renderOnNewRenderContext(function () use ($elements, $renderer) {
       return $renderer->render($elements, TRUE);
     });
     return $elements['#markup'];
  }
}

That totally would work it turns out and totally be enough to bridge the gap. This is so far the simplest implementation of all D7 render_cache and D8 Stack ...

Fabianx’s picture

CacheableResponse braindump:

- Responses should never be cached unless they implement CacheableResponseInterface, it must be easy to set render array data on a response.

This closes the gap from render arrays + out-of-band-data to Responses.

===

AttachmentsResponseInterface

=> Allows setting arbitrary #attached data on Responses that can be interpreted by FinishResponseSubscriber.

Allows deferring CSS / JS / Status Code processing to the absolute last minute by using a placeholder in html.html.twig / template_preprocess_html().

DX:

  $cache_render_array = $this->renderer->renderOnNewContext($elements);
  $response = new CacheableHtmlResponse($cache_render_array, 200);

Explicit:

  $renderer = $this->renderer;
  $cache_render_array = $this->renderer->renderOnNewContext(function() use ($elements, $renderer) { return $renderer->render($elements) });
  $response = new CacheableHtmlResponse($cache_render_array['#markup'], 200);
  $response->setCacheableMetadata(CacheableMetadata::createFromRenderArray($cache_render_array));
  $response->setAttachments($cache_render_array['#attached']);
Fabianx’s picture

Braindump:

Need a #pre_render_cache_prepare, better use #builder and a RenderArrayPreparabieBuilderInterface to support e.g. load-multiple all the comment entities for #post_render_cache of comments.

--

Can already support pseudo-parallel execution by locking via APC on blocks and waiting for another user to render the block, then can render another block first.

Any cache miss can be placeholder-ed during the same request, but should use a different queue.

--

HTTP/2 is perfect for BigPipe, both assets and other things can be streamed ...

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

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.