Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.812 diff -u -r1.812 common.inc --- includes/common.inc 29 Oct 2008 10:06:06 -0000 1.812 +++ includes/common.inc 30 Oct 2008 01:32:10 -0000 @@ -1610,6 +1610,7 @@ registry_cache_hook_implementations(FALSE, TRUE); registry_cache_path_files(); + drupal_lookup_path('footer'); } /** Index: includes/path.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/path.inc,v retrieving revision 1.28 diff -u -r1.28 path.inc --- includes/path.inc 14 Oct 2008 11:01:08 -0000 1.28 +++ includes/path.inc 30 Oct 2008 01:32:10 -0000 @@ -11,14 +11,29 @@ */ /** + * Do not cache anything. + */ +define('PATH_CACHE_NONE', 0); + +/** + * Cache everything. + */ +define('PATH_CACHE_ALL', 1); + +/** + * Cache the aliases on page that seem unchanging. + */ +define('PATH_CACHE_ADAPTIVE', 2); + +/** * Initialize the $_GET['q'] variable to the proper normal path. */ function drupal_init_path() { if (!empty($_GET['q'])) { - $_GET['q'] = drupal_get_normal_path(trim($_GET['q'], '/')); + $_GET['q'] = drupal_get_normal_path(trim($_GET['q'], '/'), '', TRUE); } else { - $_GET['q'] = drupal_get_normal_path(variable_get('site_frontpage', 'node')); + $_GET['q'] = drupal_get_normal_path(variable_get('site_frontpage', 'node'), '', TRUE); } } @@ -38,59 +53,135 @@ * Optional language code to search the path with. Defaults to the page language. * If there's no path defined for that language it will search paths without * language. + * @param $first_call + * TRUE to postpone adaptive cache initialization. Internal use only. * * @return * Either a Drupal system path, an aliased path, or FALSE if no path was * found. */ -function drupal_lookup_path($action, $path = '', $path_language = '') { +function drupal_lookup_path($action, $path = '', $path_language = '', $first_call = FALSE) { global $language; // $map is an array with language keys, holding arrays of Drupal paths to alias relations static $map = array(), $no_src = array(), $count; - - $path_language = $path_language ? $path_language : $language->language; - + // Use $count to avoid looking up paths in subsequent calls if there simply are no aliases if (!isset($count)) { $count = db_result(db_query('SELECT COUNT(pid) FROM {url_alias}')); } + if (!$count) { + return FALSE; + } + + $path_language = $path_language ? $path_language : $language->language; + $strategy = variable_get('path_strategy', PATH_CACHE_NONE); + + if ($action == 'footer' && $strategy == PATH_CACHE_ADAPTIVE) { + $insert = db_insert('path_alias_statistics_map')->fields(array('page_path', 'language', 'src', 'dst'))->delayed(); + foreach ($map as $path_language => $entry) { + foreach ($entry as $src => $dst) { + $insert->values(array('page_path' => $_GET['q'], 'language' => $path_language, 'src' => $src, 'dst' => $dst)); + } + } + $insert->execute(); + return; + } + + if ($strategy == PATH_CACHE_ALL) { + return drupal_lookup_path_cached($action, $path, $path_language); + } + if (!$map && !$first_call && $strategy == PATH_CACHE_ADAPTIVE) { + $cache = cache_get($_GET['q'] .':'. $path_language, 'cache_path'); + if ($cache) { + $map[$path_language] = $cache->data; + } + else { + $map[$path_language] = array(); + $result = db_query("SELECT src, dst FROM {path_alias_map} WHERE page_path = '%s' AND language = '%s'", $_GET['q'], $path_language); + while ($pair = db_fetch_array($result)) { + $map[$path_language][$pair['src']] = $pair['dst']; + } + cache_set($_GET['q'] .':'. $path_language, $map[$path_language], 'cache_path'); + } + } if ($action == 'wipe') { $map = array(); $no_src = array(); - $count = NULL; - } - elseif ($count > 0 && $path != '') { - if ($action == 'alias') { - if (isset($map[$path_language][$path])) { - return $map[$path_language][$path]; - } - // Get the most fitting result falling back with alias without language - $alias = db_result(db_query("SELECT dst FROM {url_alias} WHERE src = '%s' AND language IN('%s', '') ORDER BY language DESC", $path, $path_language)); - $map[$path_language][$path] = $alias; - return $alias; - } - // Check $no_src for this $path in case we've already determined that there - // isn't a path that has this alias - elseif ($action == 'source' && !isset($no_src[$path_language][$path])) { - // Look for the value $path within the cached $map - $src = ''; - if (!isset($map[$path_language]) || !($src = array_search($path, $map[$path_language]))) { - // Get the most fitting result falling back with alias without language - if ($src = db_result(db_query("SELECT src FROM {url_alias} WHERE dst = '%s' AND language IN('%s', '') ORDER BY language DESC", $path, $path_language))) { - $map[$path_language][$src] = $path; + // @TODO: This is less than optimal: the path cache needs only be wiped + // if an alias changed (and then only for a specific language), but not + // upon creation or deletion. + cache_clear_all('*', 'cache_path', TRUE); + db_query('DELETE FROM {path_alias_map}'); + db_query('DELETE FROM {path_alias_statistics_map}'); + return FALSE; + } + elseif (!empty($path)) { + switch ($action) { + case 'alias': + if (isset($map[$path_language][$path])) { + return $map[$path_language][$path]; } - else { - // We can't record anything into $map because we do not have a valid - // index and there is no need because we have not learned anything - // about any Drupal path. Thus cache to $no_src. - $no_src[$path_language][$path] = TRUE; + // Get the most fitting result falling back with alias without language + $alias = db_result(db_query("SELECT dst FROM {url_alias} WHERE src = '%s' AND language IN ('%s', '') ORDER BY language DESC", $path, $path_language)); + $map[$path_language][$path] = $alias; + return $alias; + case 'source': + // Check $no_src for this $path in case we've already determined that there + // isn't a path that has this alias + if (!isset($no_src[$path_language][$path])) { + // Look for the value $path within the cached $map + $src = ''; + if (!isset($map[$path_language]) || !($src = array_search($path, $map[$path_language]))) { + // Get the most fitting result falling back with alias without language + if ($src = db_result(db_query("SELECT src FROM {url_alias} WHERE dst = '%s' AND language IN ('%s', '') ORDER BY language DESC", $path, $path_language))) { + $map[$path_language][$src] = $path; + } + else { + // We can't record anything into $map because we do not have a valid + // index and there is no need because we have not learned anything + // about any Drupal path. Thus cache to $no_src. + $no_src[$path_language][$path] = TRUE; + } + } + return $src; } + break; + } + } + return FALSE; +} + +function drupal_lookup_path_cached($action, $path, $path_language) { + static $map = array(); + if ($action == 'wipe') { + // @TODO: see drupal_lookup_path wipe. + $map = array(); + cache_clear_all('*', 'cache_path', TRUE); + return FALSE; + } + if (empty($map)) { + $cache = cache_get('path', 'cache_path'); + if ($cache) { + $map = $cache->data; + } + else { + $map = array(); + $result = db_query("SELECT src, dst, language FROM {url_alias}"); + while ($url_alias = db_fetch_object($result)) { + $map[$url_alias->language][$url_alias->src] = $url_alias->dst; } + cache_set('path', $map, 'cache_path'); + } + } + foreach (array($path_language, '') as $language) { + if ($action == 'alias' && isset($map[$language][$path])) { + return $map[$language][$path]; + } + if ($action == 'source' && isset($map[$language]) && ($src = array_search($path, $map[$language]))) { return $src; } } - return FALSE; } @@ -121,14 +212,16 @@ * A Drupal path alias. * @param $path_language * An optional language code to look up the path in. + * @param $first_call + * TRUE to postpone adaptive cache initialization. Internal use only. * * @return * The internal path represented by the alias, or the original alias if no * internal path was found. */ -function drupal_get_normal_path($path, $path_language = '') { +function drupal_get_normal_path($path, $path_language = '', $first_call = FALSE) { $result = $path; - if ($src = drupal_lookup_path('source', $path, $path_language)) { + if ($src = drupal_lookup_path('source', $path, $path_language, $first_call)) { $result = $src; } if (function_exists('custom_url_rewrite_inbound')) { Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.272 diff -u -r1.272 system.install --- modules/system/system.install 13 Oct 2008 20:29:42 -0000 1.272 +++ modules/system/system.install 30 Oct 2008 01:32:10 -0000 @@ -619,10 +619,10 @@ $schema['cache_form'] = $schema['cache']; $schema['cache_form']['description'] = t('Cache table for the form system to store recently built forms and their storage data, to be used in subsequent page requests.'); - $schema['cache_page'] = $schema['cache']; - $schema['cache_page']['description'] = t('Cache table used to store compressed pages for anonymous users, if page caching is enabled.'); $schema['cache_menu'] = $schema['cache']; $schema['cache_menu']['description'] = t('Cache table for the menu system to store router information as well as generated link trees for various menu/page/user combinations.'); + $schema['cache_page'] = $schema['cache']; + $schema['cache_page']['description'] = t('Cache table used to store compressed pages for anonymous users, if page caching is enabled.'); $schema['cache_registry'] = $schema['cache']; $schema['cache_registry']['description'] = t('Cache table for the code registry system to remember what code files need to be loaded on any given page.'); @@ -3072,6 +3072,28 @@ } /** + * Create the cache_path table. + */ +function system_update_7012() { + $ret = array(); + $schema['cache_path'] = 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_path', $schema['cache_path']); + return $ret; +} + + +/** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000. */ Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.102 diff -u -r1.102 system.admin.inc --- modules/system/system.admin.inc 16 Oct 2008 20:23:08 -0000 1.102 +++ modules/system/system.admin.inc 30 Oct 2008 01:32:10 -0000 @@ -1337,6 +1337,7 @@ '#type' => 'fieldset', '#title' => t('Page cache'), '#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 it is viewed.'), + '#weight' => -50, ); $form['page_cache']['cache'] = array( @@ -1368,6 +1369,7 @@ '#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 each page load. If the page cache is also enabled, performance increases from enabling the block cache will mainly benefit authenticated users.'), + '#weight' => -40 ); $form['block_cache']['block_cache'] = array( @@ -1382,7 +1384,8 @@ $form['bandwidth_optimizations'] = array( '#type' => 'fieldset', '#title' => t('Bandwidth optimizations'), - '#description' => '

' . t('Drupal can automatically optimize external resources like CSS and JavaScript, which can reduce both the size and number of requests made to your website. CSS files can be aggregated and compressed into a single file, while JavaScript files are aggregated (but not compressed). These optional optimizations may reduce server load, bandwidth requirements, and page loading times.') . '

' . t('These options are disabled if you have not set up your files directory, or if your download method is set to private.') . '

' + '#description' => '

' . t('Drupal can automatically optimize external resources like CSS and JavaScript, which can reduce both the size and number of requests made to your website. CSS files can be aggregated and compressed into a single file, while JavaScript files are aggregated (but not compressed). These optional optimizations may reduce server load, bandwidth requirements, and page loading times.') . '

' . t('These options are disabled if you have not set up your files directory, or if your download method is set to private.') . '

', + '#weight' => -30, ); $directory = file_directory_path(); @@ -1408,6 +1411,7 @@ '#type' => 'fieldset', '#title' => t('Clear cached data'), '#description' => t('Caching data improves performance, but may cause problems while troubleshooting new modules, themes, or translations, if outdated information has been cached. To refresh all cached data on your site, click the button below. Warning: high-traffic sites will experience performance slowdowns while cached data is rebuilt.'), + '#weight' => -20, ); $form['clear_cache']['clear'] = array( Index: modules/path/path.module =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.module,v retrieving revision 1.149 diff -u -r1.149 path.module --- modules/path/path.module 12 Oct 2008 04:30:06 -0000 1.149 +++ modules/path/path.module 30 Oct 2008 01:32:10 -0000 @@ -234,3 +234,11 @@ function path_load($pid) { return db_fetch_array(db_query('SELECT * FROM {url_alias} WHERE pid = %d', $pid)); } + +function path_cron() { + db_query_temporary('SELECT page_path, language, :path_cutoff * MAX(c) AS cutoff FROM (SELECT page_path, language, COUNT(*) AS c FROM {path_alias_statistics_map} GROUP BY page_path, language, src, dst) AS x GROUP BY page_path, language', array('path_cutoff' => variable_get('path_cutoff', .6)), 'page_cutoff'); + db_query("ALTER TABLE page_cutoff ADD KEY path_language (page_path, language)"); + db_query('DELETE FROM {path_alias_map}'); + db_query('INSERT INTO {path_alias_map} (page_path, language, src, dst) SELECT page_path, language, src, dst FROM {path_alias_statistics_map} p GROUP BY page_path, language, src, dst HAVING COUNT(*) > (SELECT cutoff FROM page_cutoff pc WHERE pc.page_path = p.page_path AND pc.language = p.language)'); + db_query('DELETE FROM {path_alias_statistics_map}'); +} Index: modules/path/path.test =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.test,v retrieving revision 1.4 diff -u -r1.4 path.test --- modules/path/path.test 13 Oct 2008 20:57:19 -0000 1.4 +++ modules/path/path.test 30 Oct 2008 01:32:10 -0000 @@ -2,14 +2,6 @@ // $Id: path.test,v 1.4 2008/10/13 20:57:19 dries Exp $ class PathTestCase extends DrupalWebTestCase { - function getInfo() { - return array( - 'name' => t('Path alias functionality'), - 'description' => t('Add, edit, delete, and change alias and verify its consistency in the database.'), - 'group' => t('Path'), - ); - } - /** * Create user, setup permissions, log user in, and create a node. */ @@ -21,9 +13,9 @@ } /** - * Test alias functionality through the admin interfaces. + * Test alias functionality through the admin interfaces for a path caching type. */ - function testAdminAlias() { + function checkAdminAlias($type) { // create test node $node1 = $this->createNode(); @@ -35,7 +27,7 @@ // Confirm that the alias works. $this->drupalGet($edit['dst']); - $this->assertText($node1->title, 'Alias works.'); + $this->assertText($node1->title, t('@type: Alias works.', array('@type' => $type))); // Change alias. $pid = $this->getPID($edit['dst']); @@ -46,11 +38,11 @@ // Confirm that the alias works. $this->drupalGet($edit['dst']); - $this->assertText($node1->title, 'Changed alias works.'); + $this->assertText($node1->title, t('@type: Changed alias works.', array('@type' => $type))); // Confirm that previous alias no longer works. $this->drupalGet($previous); - $this->assertNoText($node1->title, 'Previous alias no longer works.'); + $this->assertNoText($node1->title, t('@type: Previous alias no longer works.', array('@type' => $type))); $this->assertResponse(404); // Create second test node. @@ -69,13 +61,13 @@ // Confirm that the alias no longer works. $this->drupalGet($edit['dst']); - $this->assertNoText($node1->title, 'Alias was successfully deleted.'); + $this->assertNoText($node1->title, t('@type: Alias was successfully deleted.', array('@type' => $type))); } /** * Test alias functionality through the node interfaces. */ - function testNodeAlias() { + function checkNodeAlias($type) { // Create test node. $node1 = $this->createNode(); @@ -86,7 +78,7 @@ // Confirm that the alias works. $this->drupalGet($edit['path']); - $this->assertText($node1->title, 'Alias works.'); + $this->assertText($node1->title, t('@type: Alias works.', array('@type' => $type))); // Change alias. $previous = $edit['path']; @@ -95,11 +87,11 @@ // Confirm that the alias works. $this->drupalGet($edit['path']); - $this->assertText($node1->title, 'Changed alias works.'); + $this->assertText($node1->title, t('@type: Changed alias works.', array('@type' => $type))); // Make sure that previous alias no longer works. $this->drupalGet($previous); - $this->assertNoText($node1->title, 'Previous alias no longer works.'); + $this->assertNoText($node1->title, t('@type: Previous alias no longer works.', array('@type' => $type))); $this->assertResponse(404); // Create second test node. @@ -110,14 +102,14 @@ $this->drupalPost('node/' . $node2->nid . '/edit', $edit, t('Save')); // Confirm that the alias didn't make a duplicate. - $this->assertText(t('The path is already in use.'), 'Attempt to moved alias was rejected.'); + $this->assertText(t('The path is already in use.'), t('@type: Attempt to moved alias was rejected.', array('@type' => $type))); // Delete alias. $this->drupalPost('node/' . $node1->nid . '/edit', array('path' => ''), t('Save')); // Confirm that the alias no longer works. $this->drupalGet($edit['path']); - $this->assertNoText($node1->title, 'Alias was successfully deleted.'); + $this->assertNoText($node1->title, t('@type: Alias was successfully deleted.', array('@type' => $type))); } function getPID($dst) { @@ -222,4 +214,3 @@ $this->assertTrue(strpos($url, $edit['path']), t('URL contains the path alias.')); } } - Index: modules/path/path.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.admin.inc,v retrieving revision 1.14 diff -u -r1.14 path.admin.inc --- modules/path/path.admin.inc 13 Oct 2008 00:33:03 -0000 1.14 +++ modules/path/path.admin.inc 30 Oct 2008 01:32:10 -0000 @@ -238,3 +238,31 @@ $path = explode('/', $_GET['q'], 5); return count($path) == 5 ? $path[4] : ''; } + +/** + * Alters the System Performance Settings form. + * + * @ingroup forms + * @see system_performance_settings() + */ +function path_form_system_performance_settings_alter(&$form) { + $options = array( + PATH_CACHE_NONE => t('Disabled'), + PATH_CACHE_ALL => t('Cache all aliases on each page (not recommended for sites with a large number of aliases)'), + PATH_CACHE_ADAPTIVE => t('Cache aliases that appear to change infrequently'), + ); + + $form['path_cache'] = array( + '#type' => 'fieldset', + '#title' => t('Path alias cache'), + '#description' => t('Path alias caching can significantly reduce the number of database queries on each page load. If the page cache is also enabled, performance increases from enabling the path alias cache will mainly benefit authenticated users.'), + '#weight' => -35, + ); + + $form['path_cache']['path_strategy'] = array( + '#type' => 'radios', + '#title' => 'Path alias cache', + '#options' => $options, + '#default_value' => variable_get('path_strategy', PATH_CACHE_NONE), + ); +}