Index: includes/bootstrap.inc =================================================================== --- includes/bootstrap.inc (revision 5912) +++ includes/bootstrap.inc (working copy) @@ -1909,3 +1909,153 @@ // @see http://bugs.php.net/bug.php?id=32330 session_set_save_handler('sess_open', 'sess_close', 'sess_read', 'sess_write', 'sess_destroy_sid', 'sess_gc'); } + +/** + * Extends ArrayObject to enable it to be used as a caching wrapper. + * + * This class should be extended by systems that need to cache large amounts + * of data and have it represented as an array to calling functions. These + * arrays can become very large, so ArrayObject is used to allow different + * strategies to be used for caching internally (lazy loading, building caches + * over time etc.). This can dramatically reduce the amount of data that needs + * to be loaded from cache backends on each request, and memory usage from + * static caches of that same data. + * + * Note that array_* functions do not work with ArrayObject. + * + * By default, the class accounts for caches where calling functions might + * request keys in the array that won't exist even after a cache rebuild. This + * prevents situations where a cache rebuild would be triggered over and over + * due to a 'missing' item. These cases are stored internally as a value of + * NULL. This means that the offsetGet() and offsetExists() methods + * must be overridden if caching an array where the top level values can + * legitimately be NULL, and where $object->offsetExists() needs to correctly + * return (equivalent to array_key_exists() vs. isset()). This should not + * be necessary in the majority of cases. + * + * Classes extending this class will usually need to override at least the + * resolveCacheMiss() method to have a working implementation. + * + * @see ThemeRegistry + */ +class CacheArrayObject extends ArrayObject { + + /** + * A cid to pass to cache_set() and cache_get(). + */ + private $cid; + + /** + * A bin to pass to cache_set() and cache_get(). + */ + private $bin; + + /** + * An array of keys to add to the cache at the end of the request. + */ + protected $add_keys = array(); + + /** + * Constructor. + * + * @param $cid + * The cid for the array being cached. + * @param $bin + * The bin to cache the array.a + */ + function __construct($cid, $bin) { + $this->cid = $cid; + $this->bin = $bin; + + if ($cached = cache_get($this->cid, $this->bin)) { + parent::__construct($cached->data); + } + else { + parent::__construct(array()); + } + } + + public function offsetExists($offset) { + if (!parent::offsetExists($offset)) { + $this->resolveCacheMiss($offset); + } + return parent::offsetGet($offset) !== NULL; + } + + public function offsetGet($offset) { + if (!parent::offsetExists($offset)) { + $this->resolveCacheMiss($offset); + } + return parent::offsetGet($offset); + } + + /** + * Add an offset and value to the ArrayObject cache. + * + * @param $offset + * The array offset that was request. + */ + public function persist($offset) { + $this->add_keys[] = $offset; + } + + /** + * Resolve a cache miss. + * + * When an offset is not found in the object, this is treated as a cache + * miss. This method allows classes implementing the interface to look up + * the actual value and allow it to be cached. + * + * @param $offset + * The offset that was requested. + */ + public function resolveCacheMiss($offset) { + // Setting the offset to NULL causes offsetGet() and offsetSet() + // to act as if the item does not exist in the array. Most implementations + // will want to override this method to consult the structure being cached + // and set offset to the value if it exists there. + $this->persist($offset, NULL); + } + + /** + * Write to the persistent cache. + * + * @param $cid + * The cache ID. + * @param $bin + * The cache bin. + * @param $data + * The data to write to the persistent cache. + * @param $lock + * Whether to acquire a lock before writing to cache. + */ + public function set($cid, $data, $bin, $lock = TRUE) { + $lock_name = $cid . ':' . $bin; + if (!$lock || lock_acquire($lock_name)) { + if ($cached = cache_get($cid, $bin)) { + $data = array_merge($cached->data, $data); + } + cache_set($cid, $data, $bin); + if ($lock) { + lock_release($lock_name); + } + } + } + + public function __destruct() { + if (!empty($this->add_keys)) { + // Since this method merges with the existing cache entry if it exists, + // ensure that only one process can update the cache item at any one time. + // This ensures that different requests can't overwrite each others' + // partial version of the cache and should help to avoid stampedes. + // When a lock cannot be acquired, the cache will not be written by + // that request. To implement locking for cache misses, override + // __construct(). + $data = array(); + foreach ($this->add_keys as $key) { + $data[$key] = parent::offsetGet($key); + } + $this->set($this->cid, $data, $this->bin); + } + } +} Index: includes/theme.inc =================================================================== --- includes/theme.inc (revision 5912) +++ includes/theme.inc (working copy) @@ -215,6 +215,80 @@ } /** + * Builds the run-time theme registry. + * + * This class extends the CacheArrayObject class to allow the theme registry to be + * accessed as a complete registry, while internally caching only the parts of + * the registry that are actually in use on the site. On cache misses the + * complete theme registry is loade and used to update the run-time cache. + */ +class ThemeRegistry Extends CacheArrayObject { + private $theme; + private $base_theme; + private $theme_engine; + private $unregistered_cid; + private $persistable; + + function __construct($theme, $base_theme = NULL, $theme_engine = NULL) { + // Make the arguments available to the rest of the class. + $this->theme = $theme; + $this->base_theme = $base_theme; + $this->theme_engine = $theme_engine; + // Maintain a global cache item for registered theme hooks. + $this->cid = 'theme_registry:runtime:' . $theme->name; + $this->bin = 'cache'; + // @todo: detect full bootstrap. + $this->persistable = TRUE; + // Maintain a per-page cache item for hooks that aren't registered. This + // will usually be un-implemented theme suggestions. Suggestions can be + // passed into theme() based on any value, so the per-page cache prevents + // any one cache item from growing to large. + $this->unregistered_cid = 'theme_registry:unregistered:' . $theme->name . ':' . hash('sha256', $_GET['q']); + + $data = array(); + if ($cached = cache_get($this->cid, $this->bin)) { + $data = $cached->data; + } + if ($cached = cache_get($this->unregistered_cid, $this->bin)) { + $data = array_merge($data, $cached->data); + } + ArrayObject::__construct($data); + } + + public function resolveCacheMiss($offset) { + $complete_registry = _theme_load_registry_complete($this->theme, $this->base_theme, $this->theme_engine); + $value = isset($complete_registry[$offset]) ? $complete_registry[$offset] : NULL; + parent::offsetSet($offset, $value); + if ($this->persistable) { + $this->persist($offset); + } + } + + public function __destruct() { + if (!empty($this->add_keys)) { + $registered = array(); + $unregistered = array(); + foreach ($this->add_keys as $offset) { + $value = $this->offsetGet($offset); + if ($value === NULL) { + $unregistered[$offset] = $value; + } + else { + $registered[$offset] = $value; + } + } + if (!empty($registered)) { + $this->set($this->cid, $registered, $this->bin); + } + if (!empty($unregistered)) { + // Don't bother locking for the per-page cache. + $this->set($this->unregistered_cid, $unregistered, $this->bin, FALSE); + } + } + } +} + +/** * Get the theme_registry cache from the database; if it doesn't exist, build * it. * @@ -227,20 +301,31 @@ * The name of the theme engine. */ function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL) { - // Check the theme registry cache; if it exists, use it. - $cache = cache_get("theme_registry:$theme->name", 'cache'); - if (isset($cache->data)) { - $registry = $cache->data; - } - else { - // If not, build one and cache it. - $registry = _theme_build_registry($theme, $base_theme, $theme_engine); - _theme_save_registry($theme, $registry); - } + $registry = new ThemeRegistry($theme, $base_theme, $theme_engine); _theme_set_registry($registry); } /** + * Load the full theme registry from cache, or rebuild it. + */ +function _theme_load_registry_complete($theme, $base_theme = NULL, $theme_engine = NULL) { + static $registry; + if (!isset($registry)) { + // Check the theme registry cache; if it exists, use it. + $cache = cache_get("theme_registry:$theme->name", 'cache'); + if (isset($cache->data)) { + $registry = $cache->data; + } + else { + // If not, build one and cache it. + $registry = _theme_build_registry($theme, $base_theme, $theme_engine); + _theme_save_registry($theme, $registry); + } + } + return $registry; +} + +/** * Write the theme_registry cache into the database. */ function _theme_save_registry($theme, $registry) {