=== modified file 'includes/common.inc' --- includes/common.inc 2008-09-20 20:22:23 +0000 +++ includes/common.inc 2008-09-26 23:30:51 +0000 @@ -1533,6 +1533,7 @@ function drupal_page_footer() { registry_cache_hook_implementations(FALSE, TRUE); registry_cache_path_files(); + drupal_lookup_path('footer'); } /** === modified file 'includes/path.inc' --- includes/path.inc 2008-06-24 22:12:15 +0000 +++ includes/path.inc 2008-09-26 23:48:50 +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) { + $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 (!$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')) { === modified file 'modules/path/path.module' --- modules/path/path.module 2008-09-17 20:37:31 +0000 +++ modules/path/path.module 2008-09-26 23:30:51 +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}'); +} === modified file 'modules/path/path.test' --- modules/path/path.test 2008-08-21 19:36:35 +0000 +++ modules/path/path.test 2008-09-26 23:30:51 +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 === modified file 'modules/system/system.admin.inc' --- modules/system/system.admin.inc 2008-09-20 20:22:23 +0000 +++ modules/system/system.admin.inc 2008-09-26 23:41:47 +0000 @@ -1388,6 +1388,29 @@ 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).'), + 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'), === modified file 'modules/system/system.install' --- modules/system/system.install 2008-09-20 20:22:23 +0000 +++ modules/system/system.install 2008-09-26 23:47:28 +0000 @@ -618,10 +618,10 @@ 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_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.'); @@ -3048,6 +3048,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. */