diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index a5dec69..8fd98b3 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -2920,6 +2920,156 @@ function drupal_static_reset($name = NULL) { } /** + * 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); + } + } +} + +/** * Detect whether the current script is running in a command-line environment. */ function drupal_is_cli() { diff --git a/includes/common.inc b/includes/common.inc index c0f2351..8da4f0d 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2332,7 +2332,7 @@ function l($text, $path, array $options = array()) { // rendering. if (variable_get('theme_link', TRUE)) { drupal_theme_initialize(); - $registry = theme_get_registry(); + $registry = theme_get_registry(FALSE); // We don't want to duplicate functionality that's in theme(), so any // hint of a module or theme doing anything at all special with the 'link' // theme hook should simply result in theme() being called. This includes diff --git a/includes/theme.inc b/includes/theme.inc index 3ae5000..f692c4c 100644 --- a/includes/theme.inc +++ b/includes/theme.inc @@ -237,18 +237,33 @@ function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callb /** * Get the theme registry. * + * @param $complete + * Optional boolean to indicate whether to return the complete theme registry + * array or an instance of the ThemeRegistry class. If TRUE, the complete + * theme registry array will be returned. This is useful if you want to + * foreach over the whole registry, use array_* functions or inspect it in a + * debugger. If FALSE, an instance of the ThemeRegistry class will be + * returned, this provides an ArrayObject which allows it to be accessed + * with array syntax, isset() and empty(), and it should be more + * lightweight than the full registry. Defaults to TRUE. + * * @return - * The theme registry array if it has been stored in memory, NULL otherwise. + * The complete theme registry array, or an instance of the ThemeRegistry + * class. */ -function theme_get_registry() { - static $theme_registry = NULL; +function theme_get_registry($complete = TRUE) { + static $theme_registry = array(); + $key = (int) $complete; - if (!isset($theme_registry)) { + if (!isset($theme_registry[$key])) { list($callback, $arguments) = _theme_registry_callback(); - $theme_registry = call_user_func_array($callback, $arguments); + if (!$complete) { + $arguments[] = FALSE; + } + $theme_registry[$key] = call_user_func_array($callback, $arguments); } - return $theme_registry; + return $theme_registry[$key]; } /** @@ -268,7 +283,7 @@ function _theme_registry_callback($callback = NULL, array $arguments = array()) } /** - * Get the theme_registry cache from the database; if it doesn't exist, build it. + * Get the theme_registry cache; if it doesn't exist, build it. * * @param $theme * The loaded $theme object as returned by list_themes(). @@ -277,23 +292,34 @@ function _theme_registry_callback($callback = NULL, array $arguments = array()) * oldest first order. * @param theme_engine * The name of the theme engine. + * @param $complete + * Whether to load the complete theme registry or an instance of the + * ThemeRegistry class. + * + * @return + * The theme registry array, or an instance of the ThemeRegistry class. */ -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; +function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL, $complete = TRUE) { + if ($complete) { + // 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); + // Only persist this registry if all modules are loaded. This assures a + // complete set of theme hooks. + if (module_load_all(NULL)) { + _theme_save_registry($theme, $registry); + } + } + return $registry; } else { - // If not, build one and cache it. - $registry = _theme_build_registry($theme, $base_theme, $theme_engine); - // Only persist this registry if all modules are loaded. This assures a - // complete set of theme hooks. - if (module_load_all(NULL)) { - _theme_save_registry($theme, $registry); - } + return new ThemeRegistry($theme, $base_theme, $theme_engine); } - return $registry; } /** @@ -313,6 +339,79 @@ function drupal_theme_rebuild() { } /** + * 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'; + $this->persistable = module_load_all(NULL); + // 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', current_path()); + + $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_get_registry(); + $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); + } + } + } +} + +/** * Process a single implementation of hook_theme(). * * @param $cache @@ -760,7 +859,7 @@ function theme($hook, $variables = array()) { if (!isset($hooks)) { drupal_theme_initialize(); - $hooks = theme_get_registry(); + $hooks = theme_get_registry(FALSE); } // If an array of hook candidates were passed, use the first one that has an diff --git a/modules/contextual/contextual.module b/modules/contextual/contextual.module index 0d6b625..2716ba2 100644 --- a/modules/contextual/contextual.module +++ b/modules/contextual/contextual.module @@ -82,16 +82,12 @@ function contextual_element_info() { * @see contextual_pre_render_links() */ function contextual_preprocess(&$variables, $hook) { - static $hooks; - // Nothing to do here if the user is not permitted to access contextual links. if (!user_access('access contextual links')) { return; } - if (!isset($hooks)) { - $hooks = theme_get_registry(); - } + $hooks = theme_get_registry(FALSE); // Determine the primary theme function argument. if (!empty($hooks[$hook]['variables'])) {