Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.788 diff -u -p -r1.788 common.inc --- includes/common.inc 21 Aug 2008 19:36:36 -0000 1.788 +++ includes/common.inc 1 Sep 2008 02:17:18 -0000 @@ -1499,6 +1499,7 @@ function drupal_page_footer() { 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.24 diff -u -p -r1.24 path.inc --- includes/path.inc 24 Jun 2008 22:12:15 -0000 1.24 +++ includes/path.inc 1 Sep 2008 02:17:18 -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,58 +53,134 @@ function drupal_init_path() { * 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) { + foreach ($map as $path_language => $entry) { + foreach ($entry as $src => $dst) { + // The new database layer will make this PostgreSQL and whatever + // compatible. Let's focus on functionality for now. + db_query("INSERT DELAYED INTO {path_alias_statistics_map} (page_path, language, src, dst) VALUES ('%s', '%s', '%s', '%s')", $_GET['q'], $path_language, $src, $dst); + } + } + return; + } + if ($strategy == PATH_CACHE_ALL) { + return drupal_lookup_path_cached($action, $path, $path_language); + } + if (!$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(); - } - 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 ($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; } @@ -120,14 +211,16 @@ function drupal_get_path_alias($path, $p * 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/path/path.module =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.module,v retrieving revision 1.144 diff -u -p -r1.144 path.module --- modules/path/path.module 21 Jun 2008 18:22:38 -0000 1.144 +++ modules/path/path.module 1 Sep 2008 02:17:19 -0000 @@ -215,3 +215,11 @@ function path_perm() { 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, %f * 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', 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.3 diff -u -p -r1.3 path.test --- modules/path/path.test 21 Aug 2008 19:36:37 -0000 1.3 +++ modules/path/path.test 1 Sep 2008 02:17:19 -0000 @@ -2,14 +2,6 @@ // $Id: path.test,v 1.3 2008/08/21 19:36:37 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 @@ class PathTestCase extends DrupalWebTest } /** - * 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 @@ class PathTestCase extends DrupalWebTest // 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 @@ class PathTestCase extends DrupalWebTest // 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 @@ class PathTestCase extends DrupalWebTest // 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 @@ class PathTestCase extends DrupalWebTest // 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 @@ class PathTestCase extends DrupalWebTest // 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 @@ class PathTestCase extends DrupalWebTest $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) { @@ -138,3 +130,87 @@ class PathTestCase extends DrupalWebTest return $node; } } + +class PathNoneTestCase extends PathTestCase { + function getInfo() { + return array( + 'name' => t('Path no cache'), + 'description' => t('Add, edit, delete, and change alias and verify its consistency in the database without path caching.'), + 'group' => t('Path'), + ); + } + + /** + * Test alias functionality through the admin interfaces for PATH_CACHE_NONE. + */ + function testAdminAliasCacheNone() { + variable_set('path_strategy', PATH_CACHE_NONE); + $this->checkAdminAlias('Path Cache None'); + } + + /** + * Test alias functionality through the node interfaces for PATH_CACHE_NONE. + */ + function testNodeAliasCacheNone() { + variable_set('path_strategy', PATH_CACHE_NONE); + $this->checkNodeAlias('Path Cache None'); + } +} + +class PathAllTestCase extends PathTestCase { + function getInfo() { + return array( + 'name' => t('Path (all cache)'), + 'description' => t('Add, edit, delete, and change alias and verify its consistency in the database with every alias cached.'), + 'group' => t('Path'), + ); + } + + /** + * Test alias functionality through the admin interfaces for PATH_CACHE_ALL. + */ + function testAdminAliasCacheAll() { + variable_set('path_strategy', PATH_CACHE_ALL); + $this->checkAdminAlias('Path Cache All'); + } + + /** + * Test alias functionality through the node interfaces for PATH_CACHE_ALL. + */ + function testNodeAliasCacheAll() { + variable_set('path_strategy', PATH_CACHE_ALL); + $this->checkNodeAlias('Path Cache All'); + } +} + +class PathAdaptiveTestCase extends PathTestCase { + function getInfo() { + return array( + 'name' => t('Path adaptive cache'), + 'description' => t('Add, edit, delete, and change alias and verify its consistency in the database with an adaptive alias strategy.'), + 'group' => t('Path'), + ); + } + + /** + * Test alias functionality through the admin interfaces for PATH_CACHE_ADAPTIVE. + */ + function testAdminAliasCacheAdaptive() { + global $db_type; + if (strpos($db_type, 'mysql') !== FALSE) { + variable_set('path_strategy', PATH_CACHE_ADAPTIVE); + $this->checkAdminAlias('Path Cache Adaptive'); + } + } + + /** + * Test alias functionality through the node interfaces for PATH_CACHE_ADAPTIVE. + */ + function testNodeAliasCacheAdaptive() { + global $db_type; + if (strpos($db_type, 'mysql') !== FALSE) { + variable_set('path_strategy', PATH_CACHE_ADAPTIVE); + $this->checkNodeAlias('Path Cache Adaptive'); + } + } +} \ No newline at end of file Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.86 diff -u -p -r1.86 system.admin.inc --- modules/system/system.admin.inc 30 Aug 2008 09:49:43 -0000 1.86 +++ modules/system/system.admin.inc 1 Sep 2008 02:17:19 -0000 @@ -1388,6 +1388,31 @@ function system_performance_settings() { '#description' => t('Note that block caching is inactive when modules defining content access restrictions are enabled.'), ); + if (module_exists('path')) { + global $db_type; + + $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).'), + ); + if (strpos($db_type, 'mysql') !== FALSE) { + $options[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.'), + ); + + $form['path_cache']['path_cache'] = array( + '#type' => 'radios', + '#title' => 'Caching', + '#options' => $options, + '#default_value' => variable_get('path_strategy', PATH_CACHE_NONE), + ); + } + $form['bandwidth_optimizations'] = array( '#type' => 'fieldset', '#title' => t('Bandwidth optimizations'), Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.262 diff -u -p -r1.262 system.install --- modules/system/system.install 30 Aug 2008 10:03:57 -0000 1.262 +++ modules/system/system.install 1 Sep 2008 02:17:19 -0000 @@ -618,10 +618,12 @@ function system_schema() { $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_path'] = $schema['cache']; + $schema['cache_path']['description'] = t('Cache table used to store path aliases.'); $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.'); @@ -3049,6 +3051,28 @@ function system_update_7010() { } /** + * Create the cache_path table. + */ +function system_update_7011() { + $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. */