Index: modules/block/block.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.admin.inc,v retrieving revision 1.5 diff -u -r1.5 block.admin.inc --- modules/block/block.admin.inc 12 Aug 2007 15:55:35 -0000 1.5 +++ modules/block/block.admin.inc 19 Aug 2007 02:03:46 -0000 @@ -262,7 +262,7 @@ foreach (list_themes() as $key => $theme) { if ($theme->status) { - db_query("INSERT INTO {blocks} (visibility, pages, custom, title, module, theme, status, weight, delta) VALUES(%d, '%s', %d, '%s', '%s', '%s', %d, %d, %d)", $form_state['values']['visibility'], trim($form_state['values']['pages']), $form_state['values']['custom'], $form_state['values']['title'], $form_state['values']['module'], $theme->name, 0, 0, $delta); + db_query("INSERT INTO {blocks} (visibility, pages, custom, title, module, theme, status, weight, delta, cache) VALUES(%d, '%s', %d, '%s', '%s', '%s', %d, %d, %d)", $form_state['values']['visibility'], trim($form_state['values']['pages']), $form_state['values']['custom'], $form_state['values']['title'], $form_state['values']['module'], $theme->name, 0, 0, $delta, BLOCK_NO_CACHE); } } Index: modules/block/block.schema =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.schema,v retrieving revision 1.1 diff -u -r1.1 block.schema --- modules/block/block.schema 25 May 2007 12:46:43 -0000 1.1 +++ modules/block/block.schema 19 Aug 2007 02:03:46 -0000 @@ -15,7 +15,8 @@ 'throttle' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny'), 'visibility' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny'), 'pages' => array('type' => 'text', 'not null' => TRUE), - 'title' => array('type' => 'varchar', 'length' => 64, 'not null' => TRUE, 'default' => '') + 'title' => array('type' => 'varchar', 'length' => 64, 'not null' => TRUE, 'default' => ''), + 'cache' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny'), ), 'primary key' => array('bid'), ); @@ -44,6 +45,8 @@ 'primary key' => array('bid'), ); + $schema['cache_block'] = drupal_get_schema_unprocessed('system', 'cache'); + return $schema; } Index: modules/block/block.module =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.module,v retrieving revision 1.274 diff -u -r1.274 block.module --- modules/block/block.module 24 Jul 2007 18:17:30 -0000 1.274 +++ modules/block/block.module 19 Aug 2007 02:16:54 -0000 @@ -13,6 +13,55 @@ define('BLOCK_REGION_NONE', -1); /** + * Constants defining cache granularity for blocks. + * + * Modules specify the caching patterns for their blocks using binary + * combinations of these constants in their hook_block(op 'list'): + * $block[delta]['cache'] = BLOCK_CACHE_PER_ROLE | BLOCK_CACHE_PER_PAGE; + * BLOCK_CACHE_PER_ROLE is used as a default when no caching pattern is + * specified. + * + * The block cache is cleared in cache_clear_all(), and uses the same clearing + * policy than page cache (node, comment, user, taxonomy added or updated...). + * Blocks requiring more fine-grained clearing might consider disabling the + * built-in block cache (BLOCK_NO_CACHE) and roll their own. + * + * Note that user 1 is excluded from block caching. + */ + +/** + * The block should not get cached. This setting should be used: + * - for simple blocks (notably those that do not perform any db query), + * where querying the db cache would be more expensive than directly generating + * the content. + * - for blocks that change too frequently. + */ +define('BLOCK_NO_CACHE', -1); + +/** +* The block can change depending on the roles the user viewing the page belongs to. +* This is the default setting, used when the block does not specify anything. +*/ +define('BLOCK_CACHE_PER_ROLE', 0x0001); + +/** +* The block can change depending on the user viewing the page. +* This setting can be resource-consuming for sites with large number of users, +* and thus should only be used when BLOCK_CACHE_PER_ROLE is not sufficient. +*/ +define('BLOCK_CACHE_PER_USER', 0x0002); + +/** +* The block can change depending on the page being viewed. +*/ +define('BLOCK_CACHE_PER_PAGE', 0x0004); + +/** + * The block is the same for every user on every page where it is visible. + */ +define('BLOCK_CACHE_GLOBAL', 0x0008); + +/** * Implementation of hook_help(). */ function block_help($path, $arg) { @@ -187,6 +236,8 @@ $result = db_query('SELECT bid, info FROM {boxes} ORDER BY info'); while ($block = db_fetch_object($result)) { $blocks[$block->bid]['info'] = $block->info; + // Not worth caching. + $blocks[$block->bid]['cache'] = BLOCK_NO_CACHE; } return $blocks; @@ -235,6 +286,8 @@ foreach ($module_blocks as $delta => $block) { $block['module'] = $module; $block['delta'] = $delta; + // If no cache pattern is specified, we use PER_ROLE as a default. + $block['cache'] = isset($block['cache']) ? $block['cache'] : BLOCK_CACHE_PER_ROLE; // If previously written to database, load values. if (!empty($old_blocks[$module][$delta])) { $block['status'] = $old_blocks[$module][$delta]->status; @@ -271,7 +324,7 @@ 'visibility' => NULL, 'throttle' => NULL, ); - db_query("INSERT INTO {blocks} (module, delta, theme, status, weight, region, visibility, pages, custom, throttle, title) VALUES ('%s', '%s', '%s', %d, %d, '%s', %d, '%s', %d, %d, '%s')", $block['module'], $block['delta'], $theme_key, $block['status'], $block['weight'], $block['region'], $block['visibility'], $block['pages'], $block['custom'], $block['throttle'], $block['title']); + db_query("INSERT INTO {blocks} (module, delta, theme, status, weight, region, visibility, pages, custom, throttle, title, cache) VALUES ('%s', '%s', '%s', %d, %d, '%s', %d, '%s', %d, %d, '%s', %d)", $block['module'], $block['delta'], $theme_key, $block['status'], $block['weight'], $block['region'], $block['visibility'], $block['pages'], $block['custom'], $block['throttle'], $block['title'], $block['cache']); } db_unlock_tables(); @@ -429,7 +482,19 @@ // Check the current throttle status and see if block should be displayed // based on server load. if (!($block->throttle && (module_invoke('throttle', 'status') > 0))) { - $array = module_invoke($block->module, 'block', 'view', $block->delta); + // Try fetching the block from cache. Block caching is not compatible with + // node_access modules. We also preserve the submission of forms in blocks, + // by fetching from cache only if the request method is 'GET'. + if (!count(module_implements('node_grants')) && $_SERVER['REQUEST_METHOD'] == 'GET' && ($cid = _block_get_cache_id($block)) && ($cache = cache_get($cid, 'cache_block'))) { + $array = $cache->data; + } + else { + $array = module_invoke($block->module, 'block', 'view', $block->delta); + if (isset($cid)) { + cache_set($cid, $array, 'cache_block', CACHE_TEMPORARY); + } + } + if (isset($array) && is_array($array)) { foreach ($array as $k => $v) { $block->$k = $v; @@ -453,3 +518,55 @@ } return $blocks[$region]; } + +/** + * Assemble the cache_id to use for a given block. + * + * The cache_id string reflects the viewing context for the current block + * instance, obtained by concatenating the relevant context information + * (user, page, ...) according to the block's cache settings (BLOCK_CACHE_* + * constants). Two block instances can use the same cached content when + * they share the same cache_id. + * + * Theme and language contexts are automatically differenciated. + * + * @param $block + * @return + * The string used as cache_id for the block. + */ +function _block_get_cache_id($block) { + global $theme, $base_root, $user; + + // User 1 being out of the regular 'roles define permissions' schema, + // it brings too many chances of having unwanted output get in the cache + // and later be served to other users. We therefore exclude user 1 from + // block caching. + if (variable_get('block_cache', 0) && $block->cache != BLOCK_NO_CACHE && $user->uid != 1) { + $cid_parts = array(); + + // Start with common sub-patterns: block identification, theme, language. + $cid_parts[] = $block->module; + $cid_parts[] = $block->delta; + $cid_parts[] = $theme; + if (module_exists('locale')) { + global $language; + $cid_parts[] = $language->language; + } + + // 'PER_ROLE' and 'PER_USER' are mutually exclusive. 'PER_USER' can be a + // resource drag for sites with many users, so when a module is being + // equivocal, we favor the less expensive 'PER_ROLE' pattern. + if ($block->cache & BLOCK_CACHE_PER_ROLE) { + $cid_parts[] = 'r.'. implode(',', array_keys($user->roles)); + } + elseif ($block->cache & BLOCK_CACHE_PER_USER) { + $cid_parts[] = "u.$user->uid"; + } + + if ($block->cache & BLOCK_CACHE_PER_PAGE) { + $cid_parts[] = $base_root . request_uri(); + } + + return implode(':', $cid_parts); + } +} \ No newline at end of file Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.870 diff -u -r1.870 node.module --- modules/node/node.module 12 Aug 2007 16:12:00 -0000 1.870 +++ modules/node/node.module 19 Aug 2007 02:03:49 -0000 @@ -770,7 +770,7 @@ // Update the node access table for this node. node_access_acquire_grants($node); - // Clear the cache so an anonymous poster can see the node being added or updated. + // Clear the page and block caches. cache_clear_all(); } @@ -1915,6 +1915,8 @@ function node_block($op = 'list', $delta = 0) { if ($op == 'list') { $blocks[0]['info'] = t('Syndicate'); + // Not worth caching. + $blocks[0]['cache'] = BLOCK_NO_CACHE; return $blocks; } else if ($op == 'view') { @@ -2502,7 +2504,7 @@ node_invoke($node, 'delete'); node_invoke_nodeapi($node, 'delete'); - // Clear the cache so an anonymous poster can see the node being deleted. + // Clear the page and block caches. cache_clear_all(); // Remove this node from the search index if needed. Index: modules/book/book.module =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.module,v retrieving revision 1.435 diff -u -r1.435 book.module --- modules/book/book.module 18 Aug 2007 11:36:40 -0000 1.435 +++ modules/book/book.module 19 Aug 2007 02:03:47 -0000 @@ -164,6 +164,7 @@ switch ($op) { case 'list': $block[0]['info'] = t('Book navigation'); + $block[0]['cache'] = BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE; return $block; case 'view': if (arg(0) == 'node' && is_numeric(arg(1))) { Index: modules/statistics/statistics.module =================================================================== RCS file: /cvs/drupal/drupal/modules/statistics/statistics.module,v retrieving revision 1.264 diff -u -r1.264 statistics.module --- modules/statistics/statistics.module 12 Aug 2007 15:55:36 -0000 1.264 +++ modules/statistics/statistics.module 19 Aug 2007 02:03:50 -0000 @@ -502,6 +502,8 @@ case 'list': if (variable_get('statistics_count_content_views', 0)) { $blocks[0]['info'] = t('Popular content'); + // Too dynamic to cache. + $blocks[0]['cache'] = BLOCK_NO_CACHE; return $blocks; } break; Index: modules/menu/menu.module =================================================================== RCS file: /cvs/drupal/drupal/modules/menu/menu.module,v retrieving revision 1.134 diff -u -r1.134 menu.module --- modules/menu/menu.module 16 Aug 2007 12:47:34 -0000 1.134 +++ modules/menu/menu.module 19 Aug 2007 02:03:48 -0000 @@ -666,6 +666,9 @@ foreach ($custom_menus as $name => $title) { // Default "Navigation" block is handled by user.module. $blocks[$name]['info'] = check_plain($title); + // Menu blocks can't be cached because each menu item can have + // a custom access callback. menu.inc manages its own caching. + $blocks[$name]['cache'] = BLOCK_NO_CACHE; } return $blocks; } Index: modules/locale/locale.module =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v retrieving revision 1.187 diff -u -r1.187 locale.module --- modules/locale/locale.module 3 Jul 2007 16:27:51 -0000 1.187 +++ modules/locale/locale.module 19 Aug 2007 02:03:47 -0000 @@ -516,6 +516,8 @@ function locale_block($op = 'list', $delta = 0) { if ($op == 'list') { $block[0]['info'] = t('Language switcher'); + // Not worth caching. + $block[0]['cache'] = BLOCK_NO_CACHE; return $block; } Index: includes/cache.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/cache.inc,v retrieving revision 1.12 diff -u -r1.12 cache.inc --- includes/cache.inc 25 May 2007 21:01:30 -0000 1.12 +++ includes/cache.inc 19 Aug 2007 02:03:46 -0000 @@ -114,7 +114,7 @@ /** * * Expire data from the cache. If called without arguments, expirable - * entries will be cleared from the cache_page table. + * entries will be cleared from the cache_page and cache_block tables. * * @param $cid * If set, the cache ID to delete. Otherwise, all cache entries that can @@ -134,6 +134,7 @@ if (!isset($cid) && !isset($table)) { cache_clear_all(NULL, 'cache_page'); + cache_clear_all(NULL, 'cache_block'); return; } Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.519 diff -u -r1.519 system.module --- modules/system/system.module 18 Aug 2007 20:08:33 -0000 1.519 +++ modules/system/system.module 19 Aug 2007 02:03:53 -0000 @@ -683,7 +683,7 @@ $form['page_cache'] = array( '#type' => 'fieldset', '#title' => t('Page cache'), - '#description' => t('Enabling the cache will offer a significant performance boost. Drupal can store and send compressed cached pages requested by anonymous users. By caching a web page, Drupal does not have to construct the page each time someone wants to view it.'), + '#description' => t('Enabling the page cache will offer a significant performance boost. Drupal can store and send compressed cached pages requested by anonymous users. By caching a web page, Drupal does not have to construct the page each time someone wants to view it.'), ); $form['page_cache']['cache'] = array( @@ -701,7 +701,22 @@ '#title' => t('Minimum cache lifetime'), '#default_value' => variable_get('cache_lifetime', 0), '#options' => $period, - '#description' => t('On high-traffic sites it can become necessary to enforce a minimum cache lifetime. The minimum cache lifetime is the minimum amount of time that will go by before the cache is emptied and recreated. A larger minimum cache lifetime offers better performance, but users will not see new content for a longer period of time.') + '#description' => t('On high-traffic sites it can become necessary to enforce a minimum cache lifetime. The minimum cache lifetime is the minimum amount of time that will go by before the cache is emptied and recreated. A larger minimum cache lifetime offers better performance, but users will not see new content for a longer period of time. This setting also affects block caching.') + ); + + $form['block_cache'] = array( + '#type' => 'fieldset', + '#title' => t('Block cache'), + '#description' => t('Enabling the block cache can offer a performance increase for all users by preventing blocks from being reconstructed on every page load. If page cache is also enabled, this performance increase will mainly affect authenticated users.'), + ); + + $form['block_cache']['block_cache'] = array( + '#type' => 'radios', + '#title' => t('Block cache'), + '#default_value' => variable_get('block_cache', CACHE_DISABLED), + '#options' => array(CACHE_DISABLED => t('Disabled'), CACHE_NORMAL => t('Enabled (recommended)')), + '#disabled' => count(module_implements('node_grants')), + '#description' => t('Note that block caching is inactive when modules defining content access restrictions are enabled.'), ); $form['bandwidth_optimizations'] = array( @@ -1275,8 +1290,8 @@ if (!array_key_exists($block['region'], $regions)) { $block['region'] = system_default_region($theme); } - db_query("INSERT INTO {blocks} (module, delta, theme, status, weight, region, visibility, pages, custom, throttle) VALUES ('%s', '%s', '%s', %d, %d, '%s', %d, '%s', %d, %d)", - $block['module'], $block['delta'], $theme, $block['status'], $block['weight'], $block['region'], $block['visibility'], $block['pages'], $block['custom'], $block['throttle']); + db_query("INSERT INTO {blocks} (module, delta, theme, status, weight, region, visibility, pages, custom, throttle, cache) VALUES ('%s', '%s', '%s', %d, %d, '%s', %d, '%s', %d, %d, %d)", + $block['module'], $block['delta'], $theme, $block['status'], $block['weight'], $block['region'], $block['visibility'], $block['pages'], $block['custom'], $block['throttle'], $block['cache']); } } } Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.136 diff -u -r1.136 system.install --- modules/system/system.install 18 Aug 2007 20:03:19 -0000 1.136 +++ modules/system/system.install 19 Aug 2007 02:03:52 -0000 @@ -3481,6 +3481,52 @@ } /** + * Add block cache. + */ +function system_update_6027() { + $ret = array(); + + // Create the blocks.cache column. + db_add_field($ret, 'blocks', 'cache', array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny')); + + // Create the cache_block table. + $schema['cache_block'] = array( + 'fields' => array( + 'cid' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + 'data' => array('type' => 'blob', 'not null' => FALSE, 'size' => 'big'), + 'expire' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + 'created' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + 'headers' => array('type' => 'text', 'not null' => FALSE), + 'serialized' => array('type' => 'int', 'size' => 'small', 'not null' => TRUE, 'default' => 0) + ), + 'indexes' => array('expire' => array('expire')), + 'primary key' => array('cid'), + ); + db_create_table($ret, 'cache_block', $schema['cache_block']); + + // Fill in the values for the new 'cache' column, + // by refreshing the {blocks} table. + global $theme, $custom_theme; + $old_theme = $theme; + $themes = list_themes(); + + $result = db_query("SELECT DISTINCT theme FROM {blocks}"); + while ($row = db_fetch_array($result)) { + if (array_key_exists($row['theme'], $themes)) { + // Set up global values so that _blocks_rehash() + // operates on the expected theme. + $theme = NULL; + $custom_theme = $row['theme']; + _block_rehash(); + } + } + + $theme = $old_theme; + + return $ret; +} + +/** * @} End of "defgroup updates-5.x-to-6.x" * The next series of updates should start at 7000. */ Index: modules/search/search.module =================================================================== RCS file: /cvs/drupal/drupal/modules/search/search.module,v retrieving revision 1.229 diff -u -r1.229 search.module --- modules/search/search.module 8 Jul 2007 12:18:02 -0000 1.229 +++ modules/search/search.module 19 Aug 2007 02:03:50 -0000 @@ -144,6 +144,8 @@ function search_block($op = 'list', $delta = 0) { if ($op == 'list') { $blocks[0]['info'] = t('Search form'); + // Not worth caching. + $blocks[0]['cache'] = BLOCK_NO_CACHE; return $blocks; } else if ($op == 'view' && user_access('search content')) { Index: CHANGELOG.txt =================================================================== RCS file: /cvs/drupal/drupal/CHANGELOG.txt,v retrieving revision 1.218 diff -u -r1.218 CHANGELOG.txt --- CHANGELOG.txt 16 Jul 2007 09:29:43 -0000 1.218 +++ CHANGELOG.txt 19 Aug 2007 02:03:46 -0000 @@ -54,6 +54,7 @@ * Made it easier to conditionally load include files. * Added a JavaScript aggregator and compressor. * Made Drupal work correctly when running behind a reverse proxy like Squid or Pound. + * Added block-level caching, improving performance for logged-in users. - File handling improvements: * Entries in the files table are now keyed to a user, and not a node. * Added re-usable validation functions to check for uploaded file sizes, extensions, and image resolution. Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.833 diff -u -r1.833 user.module --- modules/user/user.module 18 Aug 2007 20:03:19 -0000 1.833 +++ modules/user/user.module 19 Aug 2007 02:03:55 -0000 @@ -627,10 +627,19 @@ if ($op == 'list') { $blocks[0]['info'] = t('User login'); + // Not worth caching. + $blocks[0]['cache'] = BLOCK_NO_CACHE; + $blocks[1]['info'] = t('Navigation'); + // Menu blocks can't be cached because each menu item can have + // a custom access callback. menu.inc manages its own caching. + $blocks[1]['cache'] = BLOCK_NO_CACHE; + $blocks[2]['info'] = t('Who\'s new'); - $blocks[3]['info'] = t('Who\'s online'); + // Too dynamic to cache. + $blocks[3]['info'] = t('Who\'s online'); + $blocks[3]['cache'] = BLOCK_NO_CACHE; return $blocks; } else if ($op == 'configure' && $delta == 2) { Index: modules/profile/profile.module =================================================================== RCS file: /cvs/drupal/drupal/modules/profile/profile.module,v retrieving revision 1.215 diff -u -r1.215 profile.module --- modules/profile/profile.module 2 Aug 2007 10:36:42 -0000 1.215 +++ modules/profile/profile.module 19 Aug 2007 02:03:49 -0000 @@ -125,7 +125,7 @@ if ($op == 'list') { $blocks[0]['info'] = t('Author information'); - + $blocks[0]['cache'] = BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE; return $blocks; } else if ($op == 'configure' && $delta == 0) {