(Part 1 of the "Site Performance with Drupal 8 and Symfony" blog series)
(This article does not represent the current state of Drupal 8 development.)
In over five years of developing Drupal, the most interesting announcement I’ve read was that the SCOTCH and WSCII initiatives had resulted in refactoring Drupal 8 code to use Symfony components. The switch comes with a lot of fascinating possibilities for performance improvements, but on the other hand, frameworks are often maligned for being bloated. Just how much could adding Symfony code improve performance?
Drupal 8 hasn’t been released yet, but the adoption of Symfony has already let it pull in some beautiful pieces of architecture such as its robust sub-request framework. If that doesn’t strike you as much to write home about, consider the impact of sub-requests on caching. Drupal uses full page caching, and it’s often deployed using other caches like Varnish and content delivery networks (CDNs). These approaches cope poorly with logged-in users because the content served to them is so heavily customized. Edge Side Includes (ESI) can fix this by selectively passing through only uncacheable portions of the page to the backend, but they remain underutilized because most web frameworks can’t readily render just a portion of a page.
Symfony is well-suited to leverage ESI because any component of a page (such as a block or a sidebar) can be built as a sub-request for little additional developer effort. Drupal 8 will inherit this capability, and a year from now, large-scale Drupal 8 sites may bypass common performance pain points by taking an ‘ESI Everywhere’ approach and leaving CDN servers to do the work of assembling a dynamic page from a static cache entry.
Finding performance problems that only manifest at scale is very difficult to do with traditional tools like profilers and debuggers. While AppNeta TraceView already works out of the box on Drupal 8, it was lacking the support of our community-supported Drupal module. I decided to port the module to Drupal 8 as soon as possible so that I could collect additional data to answer questions about its performance at scale.
Like most Drupal developers, though, I’d never tried writing Symfony code or using Composer to manage packages. Before coding I decided to research both Symfony in its own right and how it’s being leveraged to rewrite Drupal. The reframeworking brings many changes, but the shift away from Drupal’s familiar hook-based event system to Symfony’s event listener system caught me off guard.
It’s justified, though, as there are some serious advantages to the switch such as not needing to check every module for a hook implementation. Developers will also have much finer control over how modules respond to events, rather than having to use unusual hooks simply because they get called at the ‘right’ point in page loading (I’m looking at you, `hook_footer`!).
After a relatively painless porting process, roughly following the process detailed here, I had the module running. I decided to split a Symfony bundle off of it so that much of the functionality could be used in other applications. In writing that bundle I still relied on the strategy used by our Drupal module, which monitors layers like `drupal_views` by installing two additional modules: an ‘early’ module with a very low weight and a ‘late’ module with a very high weight. As each hook was removed from core, I moved its implementations from the module into the bundle and tagged that event with listeners at maximum and minimum priority.
This mostly worked, but some traces failed to load, and inspecting raw data revealed entry events with no matching exit event. When I looked into this problem deeper, I realized that I had forgotten about a key difference between Drupal hooks and Symfony event listeners: unlike Drupal hooks, which always run to completion, Symfony event listeners can call `stopPropagation` at any time to ‘consume’ the event and prevent lower-priority listeners from responding!
The right approach is to override how Symfony’s events work, but that’s far outside the scope of an alpha. For now, I introduced a workaround by turning the low-priority `view` listener into a high-priority listener on `response`, the event immediately afterwards. Soon I had a reasonably working Drupal 8 module and Symfony bundle, and it became noticeably easier to understand where specific queries fit into the request flow.
The Symfony bundle can be found on AppNeta’s Github account Github account, I’ll be posting the module alpha on the issue queue after some internal cleanup. I encourage you to use them. Making the most of Symfony in ways like this will help us make Drupal 8 the speed champion of the CMS landscape.