Blocks, caching, and context

Crell's picture

David Strauss was in Chicago for Tek-X last week, so I managed to grab him for a few hours to chew over the question of caching and performance. It was an extremely productive meeting so I'm going go try and summarize our notes here for posterity and further chewing.

Here's the basic challenge:

For ESI caching (Varnish, et al) to work, we need to be able to isolate a specific Block to load and render it, with the context information it should have, independently of the rest of the page. For example, the ideal case is that we could serve a fully cached page to everyone, including authenticated users, except for the Navigation block that has user-sensitive information such as selected menu items they can see, the "Hello, $username!" string, etc. Then the page gets cached in Varnish, and on subsequent page requests all Varnish does is call back to Drupal to render just that one block for the current user then drop that block into its cached page and print it back to the browser. All Drupal is doing at that point is rendering user-context-sensitive blocks; the rest of the page it almost never cares about.

For that to work, though, we absolutely must be able to know what context information is appropriate for that block, and what information to send it. Reconstructing the entire context object is needlessly expensive and wasteful. That is, we'd want to have a URL such as /esi/$block_id?uid=5&path=/some/page/here. That URL would result in rendering the block configured as $block_id (say, the Navigation menu block plus its configuration) with the context of user 5 and a path of /some/page/here. The entire rest of the page process is skipped, and we can then go about streamlining bootstrap to do less work. It's actually the exact same callback that would be used for AHAH rendering of blocks. Both Varnish and Drupal can then do additional caching of that rendered block as appropriate.

In practice the URL would be less human-friendly than that, and probably have a serialized or URL-encoded context array rather than separate keys. That's an implmentation detail, though. I just mention it for completeness.

In order for that to work, we need a way to know exactly what context $block_id is going to use. And not just that block, but all of the things that block may end up calling.

As an aside, this is why it's imperative that we only support injected context. Context that cannot be injected becomes incompatible with Varnish, AHAH callbacks, and pretty much anything else that bypasses the "render the entire everything" process. That's going to be a lot of things...

There are two ways that we could determine what context a given block users: Require the block author to specify it explicitly in the defintion hook or derive it automatically. Both have drawbacks. Explicit specification puts a large burden on the module author. Deriving it is more complex, and if some context is only used conditionally (the user has role X only if the current OG is Y) it could get missed.

However, it is possible to do both. That is, derive for most context and allow the block author to also specify context keys that may not derive easily. Context is relevant for a give block if it matches either criteria.

In essence, then, the context keys and given values that a given block users become its cache key, and that cache key can be used by Varnish, Drupal's own caching system, or anything else. That's much more robust than trying to cache on derived SQL strings as Drupal 7 attempts to do, and gives us much more flexibility to do "fancy stuff" with Blocks since we know so much more about them. That also means partial page caching becomes really really easy, even without Varnish.

David went so far as to suggest that we could do a poor-man's ESI within Drupal's own caching system by caching the page with ESI tags in it, and then running a pre-process on it to find the ESI tags and do the same single-block-render routine all in PHP without having a second page request. It would just be a rouine that mocks up a context object to match the ESI tag and renders that one block, then does a str_replace(). (That makes context mocking even more critical, and not just for testing.)

Note: The code in the sections that follow is there only because it's the easiest way to explain what we talked about, and it should be views as pseudo-code using PHP syntax, not as actual code implementations.

That has implications for how the context system works. Specifically, we would need a way to round-trip context keys from key (primary_node) to value (nid 12) to loaded value (the $node object) and back again, because we do not want to serialize the entire $node into the context cache string, just its nid. The easiest way to do that is to make the context callback for each key an object, and give it methods to return whichever of those keys we want at any given time.

David also made the suggestion that we can avoid the overhead of a registry hook with magic class naming, much as we do for database drivers. The only downside is that, like DB drivers, they would most likely have funky names with underscores in them, which is otherwise a coding standards violation.

That is, the primary node context would work something like this:

<?php
$node
= $context->get('primary_node');

class
Context {

  public function
get($key) {
   
$class = "Context_" . $key;
   
$handler = new $class($this);
    return
$handler->getValue();
  }

}

class
Context_primary_node implements ContextInterface {

  public function
getValue() {
   
$nid = $this->context->arg(1);
    return
node_load($nid);
  }

  public function
getIdentifier() {
    return
$this->getValue()->nid;
  }
}
?>

Give or take lots of caching. And then there'd be some mechanism (which I'm not thinking through yet) to insert that nid into Context_primary_node in place of checking the URL.

As a side effect, registering a new context responder becomes dead simple: Write a class with the right name and poof. It also prevents duplicates; that would cause a PHP parser error. :-) (Actually it might not since the classes would autoload, but it becomes an issue for the autoloader to sort out rather than the context system.)

In order to derive the used context keys, we'd need to introduce an extra thin layer in the block rendering. Remember that the context needed by a block isn't just that block, it's that block plus all of the blocks underneath it (if it's a Block Region). For that, we'd need to split the "render a child block" process into two parts. It's easier to just show it in code, I think:

<?php
class BlockRegion extends Block {

  public function
render() {
    foreach (
$this->blocks as $block_info) {
     
$block = new $block_info['class']($this->context);
     
$rendered[$block_info['block_id']] = $block->generate();
     
$this->usedContext += $block->getUsedContext();

     
// Use $rendered and the configured template/layout to
      // generate the string of this "block".
   
}
  }

}

abstract class
Block {
  public function
generate() {
   
$this->context->startTracking();
   
$output = $this->render();
   
$this->usedContext = $this->context->stopTracking();
    return
$output;
  }

 
// Individual block classes would implement this as appropriate.
 
abstract public function render();

  public function
getUsedContext() {
   
// The important context is whatever we detected plus
    // whatever the block author said was important.
   
return array_merge($this->usedContext, $this->info_hook['context_keys']);
  }
}
?>

And then a given context object would have an internal system to track when a given context key is requested vis a vis it's tracking layers.

David, please feel free to correct me if I got something wrong or misrepresented something. :-)

Any more chewing to do?

Comments

Wow, looks great!

dmitrig01's picture

This looks great. Completely makes sense.

Great overview. Some problems :)

sdboyer's picture

There's a lot in this post that's in line with my thinking thus far on building an effective ESI framework. There are some particular points I'd like to pull out & highlight as really important:

  1. We should be thinking about sharing as much logic between 'selective block renderers' like ESI, AHAH, and others.

  2. We HAVE to support only injected context, as that's what gives us granular control over the execution environment. Not having it will severely hamper our ability to do any selective operation that demands full context; ESI/AHAH are potentially only one example of this.

While I'm with a lot of the overall spirit, here, I do take issue with some of it. Lemme pick this quote out to kick it off:

There are two ways that we could determine what context a given block users: Require the block author to specify it explicitly in the definition hook or derive it automatically. Both have drawbacks. Explicit specification puts a large burden on the module author. Deriving it is more complex, and if some context is only used conditionally (the user has role X only if the current OG is Y) it could get missed.

I don't believe we actually have this option here, based on lessons we've learned in Panels. For blocks to be fully portable, the best they can do is specify that they need a particular 'type' of context data (e.g., a node or an OG, a user, a term), without caring about how that actual context data is generated. So, if it needs a node, the block's logic can't care if that node data is derived directly from an argument (path = node/42) or from a web of nested noderefs. In other words, it doesn't just put "a large burden on the module author" - it would be an architecture that severely limits the reusability of blocks. Why should a block for rendering node content care have anything to say about where that node data comes from?

Panels does its blocks right now with two parts here: a) hardcoded properties in the block definition and b) db-saved configuration that's specific to a given block instance. The former specifies the types of context the block looks for ('slots' of a sort); the latter contains the mapping between fully realized incoming context data and those 'slots' specified by the block. Now I'd love to see a heuristic system that could be better at matching available context data to block-defined slots, but I haven't yet been able to brainstorm a system that could do it very well. My biggest impediment there, though, is not having had a good, smart centralized context system to work from. So I'm hoping that's surmountable.

To this end, I also think your first block of pseudo-code is off-target. A class for a 'primary' node doesn't make sense, as that's munging together both context content/type and source. Panels figured out a long time ago, I think to our great benefit, that these should be separated. I started writing some pseudo-code, but haven't finished it up yet...gah, I'll have to get back to that and post it later.

Now, lemme try to avoid just being a naysayer: While I know that approaches to ESI or selective rendering have generally involved the creation of special URLs in the past, I think intelligent use of context could obviate the need for that. To explain, let me situate this in a wider view by focusing on the difference between what's happening on a standard page request versus an ESI request. I've already done a bit of this over in my other post, but let me dig in a bit more here.

A standard HTML request with blocks loads up context, loads up an RC/DC pair (that is, class(es) to use + conf to pass to them) and loads up blocks to be rendered. The DC iterates through the blocks and mocks the context to match what each block is configured to look for, then triggers their render method with the mocked context; caching is also interacted with at some point in there. Rendered data is stored into probably an array as it's generated, which is then returned out when rendering is complete.

An ESI, or AHAH, or any other selective block rendering request will, ideally, do a reduced-logic context build & bootstrap, use a simpler RC/DC combo, load up the specified block's conf, mock the context as necessary to simulate the block's needed environment, render the block (potentially by retrieving from cache), then return the rendered data.

What really differentiates these is a) the idea of a "reduced" bootstrap and initial context build, and b) the RC/DC set that's used to control the render process. The question is, do we really need to move things over to an entirely different query path to make all those gains? Or would it be easier to just overlay selective requests onto the base request path itself, then work from additional GET parameters to trim down bootstrap/context build? I think the latter would be easier, especially given that ESI requests are more similar than different from their standard request parent - they need to inherit access controls, more or less of the context, and DC/block configuration, and their bootstrap needs are generally similar (see my other post for thoughts on that). Maybe most importantly, unless things have changed and I missed the memo, we've been talking about architecting context so it triggers data loading only when needed.

In my mind, all of this adds up to a goal that we make selective rendering something that's done by tacking additional data onto an existing request vector, rather than creating a whole new form of request vector which has to then replicate the original. I think which is most effective and efficient will ultimately be determined by our approach to implementing the context system.

No memo

Crell's picture

You didn't miss the memo. :-) We're still lazy-loading context information here, but I think the take away is that some context may have two forms: The reference form (nid) and the use form ($node). We only want to store (for ESI, block cache IDs, etc.) the reference form and lazy re-derive the use form as needed.

For the rest, let's talk on our call in a half hour. :-)

This looks good to me too, a

catch's picture

This looks good to me too, a few extra things:

The 'reduced' bootstrap sounds a lot like page caching to me - if we always have a unique path then it ought to be possible to re-use much the same mechanism. This doesn't preclude refactoring the bootstrap process (and I have plans in the works for some of this), but we already have something available which would work within that limitation.

If we're able to identify up-front all the possible context that can be given to page fragments (I say page fragments rather than blocks, because I'd also like to see node teaser rendering and plenty of other things doable with ESI, baby steps towards this are in performance_hacks module), then that helps for two things if you have a reasonably finite number of cache keys - 1. pre-generation/write-through caching 2. targeted clearing of caches.

Pre-generation & Clearing of caches

mikeytown2's picture

I do this at the page level with Boost & it works remarkably well. Only thing is views is a little bit of an issue for 2 reasons: Pagers & Memory Usage. I'm working on the next version of the internal boost logic and it has a decent context gathering, so that should take care of pagers & arguments. I think there is a memory leak with views; but that only effects feeds. For feeds I could have 100 nodes that need to be checked in 200 different view displays; I've seen it eat over 1.5GB of ram.

Anyway back to the subject at hand; even doing "cache keys" at the page level can get out of hand. I've had several reports of databases falling over trying to keep up with recording where all the content shows up on every page. Long story short I need to make this smarter and only update if any one of the nodes being used has changed since last time OR if a new node is present. Once again an issues with things like views and panels (displaying nodes on a single page).

It's all doable we just need a lot of iterations of the code so it's mostly right. In my dealings with boost I got one thing mostly right: multi-threading, even on shared hosting; close to right: the framework for page level cache expiration via a database table (boost_cache_relationship).

Thoughts from the Boost module

mikeytown2's picture

Not sure where to add this, so here looks like a good spot...

In boost I use 3 items to identify where the cached page came from.
page_callback - Direct from the menu_router table
page_type - node type; vocab name; user roles; view name; etc...
page_id - nid; vid; uid; view display; etc...
I will need to add in a 4th soon for pagers for views.
I use this to determine all the views that contain a node, the ability to flush certain parts of the page cache, & it allows for custom cache expiration times.

In short, using something like menu_get_item() as the starting point for setting the context is a good idea IMHO.

You should probably have a

sdboyer's picture

You should probably have a looksee at some of the other stuff that's been posted about context in here - probably http://groups.drupal.org/node/67583 is a good spot. menu_get_item() is a ways further down the critical path than context setting happens - context setting is something more along the lines of an early bootstrap-level operation, at least in my current thinking. Boost will...well, yeah, need to be updated for this framework :) It'll probably be a very different place that it ends up fitting in.

Bringing up menu_get_item() is an interesting point, though...what will the analogue to it be? Certainly I think we should be thinking about shorthand methods that essentially mock context for you, given some inputs.

I think it will be like

dmitrig01's picture

I think it will be like ->getCurrentNode or something like that (whatever the notation is).

Interesting little practical discussion...

sdboyer's picture

On the note of individually-refreshable/renderable items, such as what we're talking about in the abstract here: http://drupal.org/node/760554

There's not that much there that goes too deep into the theory, but the basic problem is the same - how do we identify, isolate, and simulate the context for a single block-level element, then return it?

Something that came up during

catch's picture

Something that came up during the entity discussions at DrupalCon that's relevant for this. We were discussing render caching of entities. performance_hacks has some not very nice code that swaps out node_view() etc. for render caching versions, however that currently still needs to be passed a loaded node for context.

The pattern we're slowly standardizing on in D7 is EntityFieldQuery (or SQL), multiple load the objects from IDs, then render.

A step further from this would be EntityFieldQuery -> IDS -> straight to render. That way we'd have ID, entity type, bundle, view mode - which should be enough context to generate a cache string in most cases. We'd then get straight from an ID to a cached string with very little intermediate work - meaning a very tiny amount of data needing to be loaded from the database or cache_get(), and extremely low memory usage on a cache_get() - it also means that setups that try to fetch ESI direct from memcache, bypassing Drupal would be on an even playing field (more or less, at least it'd be a step closer). We could keep the 'multiple' nature of this so that a list of node titles could be constructed from separate cache entries, but a single multiple cache get, or a mixture of one cache_get_multiple() and one node_load_multiple().

This causes some issues though - for example it's legitimate to want to skip caching if the user is the author of an entity and things like that which require the entity for context, we may then need a system that allows elements/formatters/render plugins to define what context they need - which would need to be stored somewhere so you could determine whether to load the additional context or not.

Just to update this issue, a

moshe weitzman's picture

Just to update this issue, a cache_tags module has arisen out of a patch for Drupal 8. Start at http://drupal.org/project/cache_tags.

I'm seeing a 404 on that

xtfer's picture

I'm seeing a 404 on that link...

No _

Thank you

xtfer's picture

Thank you

Web Services and Context Core Initiative

Group organizers

Group notifications

This group offers an RSS feed. Or subscribe to these personalized, sitewide feeds:

Hot content this week