IMPORTANT!!! If you want to use this solution do it with caution and provide a constant that can flag the caching code on/off.
The solution I show here is not of settings or tuning of some settings, but do some change in the code of the core in order to make Drupal 7 works much faster.
During development I noticed that many request, even the most simply ones has a latency of 5 seconds. This is frustrating, especially when you need to do lots of settings such as editing views.
At first I thought that the problem is related with general issue found by many developers when working with Apace on local host, but changing one solution after another didn't changed the slow behavior.
I decided to debug, and then step by step I narrowed down the code section where most of the delay (4 seconds) occur.
I an using with ECK module to create entities for business-logic. And I have quite few entities. The latency in my case was caused here:
eck.module :: eck_entity_info and especially in the foreach loop when collecting entity info from all hooks into one array:
foreach (EntityType::loadAll() as $entity_type) {
// eck__entity_info creates the entity_info for each entity type.
$info = array_merge($info, eck__entity_type__info($entity_type));
}
I took some measurements and noticed that there is no one single module that cause the delay but few modules create a compound time of 4 seconds added to each new request that use this entity_get_info function.
What I did is to create a caching file with serialized result of the foreach loop. The file is created once and deleted by the eck_flush_caches hook implementation. This solution eliminated the latency caused by this section in the code. Now a simple GET request that took 7 seconds has become 2.4 seconds.
I took this solution one step ahead and instead of caching only the result for eck_entity_info, I moved the caching to the top level when all entity info from all modules is collected in file: common.inc.
Now the request time was reduced to 1.3 second!!! Using Drupal is fun again.
Note: I am not saying that this solution is good for all but in the case that you are using many entities and the settings for these entities are not dynamically change every request, you may want to consider this solution.
I am attaching two changes made in common.inc file:
function entity_get_info($entity_type = NULL) {
global $language;
// Use the advanced drupal_static() pattern, since this is called very often.
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['entity_info'] = &drupal_static(__FUNCTION__);
}
$entity_info = &$drupal_static_fast['entity_info'];
// hook_entity_info() includes translated strings, so each language is cached
// separately.
$langcode = $language->language;
if (empty($entity_info)) {
if ($cache = cache_get("entity_info:$langcode")) {
$entity_info = $cache->data;
}
else {
if (defined("CACHE_GET_ENTITY_INFO") && CACHE_GET_ENTITY_INFO && defined("CACHE_DIR")) {
$cacheFile = CACHE_DIR . "/entity_get_info.phps";
$cachedData = file_exists($cacheFile) ? file_get_contents($cacheFile) : false;
if(! $cachedData) {
$entity_info = module_invoke_all('entity_info');
file_put_contents($cacheFile, serialize($entity_info));
} else {
$entity_info = unserialize($cachedData);
}
} else {
$entity_info = module_invoke_all('entity_info');
}
// Merge in default values.
foreach ($entity_info as $name => $data) {
$entity_info[$name] += array(
'fieldable' => FALSE,
'controller class' => 'DrupalDefaultEntityController',
'static cache' => TRUE,
'field cache' => TRUE,
'load hook' => $name . '_load',
'bundles' => array(),
'view modes' => array(),
'entity keys' => array(),
'translation' => array(),
);
$entity_info[$name]['entity keys'] += array(
'revision' => '',
'bundle' => '',
);
foreach ($entity_info[$name]['view modes'] as $view_mode => $view_mode_info) {
$entity_info[$name]['view modes'][$view_mode] += array(
'custom settings' => FALSE,
);
}
// If no bundle key is provided, assume a single bundle, named after
// the entity type.
if (empty($entity_info[$name]['entity keys']['bundle']) && empty($entity_info[$name]['bundles'])) {
$entity_info[$name]['bundles'] = array($name => array('label' => $entity_info[$name]['label']));
}
// Prepare entity schema fields SQL info for
// DrupalEntityControllerInterface::buildQuery().
if (isset($entity_info[$name]['base table'])) {
$entity_info[$name]['base table field types'] = drupal_schema_field_types($entity_info[$name]['base table']);
$entity_info[$name]['schema_fields_sql']['base table'] = drupal_schema_fields_sql($entity_info[$name]['base table']);
if (isset($entity_info[$name]['revision table'])) {
$entity_info[$name]['schema_fields_sql']['revision table'] = drupal_schema_fields_sql($entity_info[$name]['revision table']);
}
}
}
// Let other modules alter the entity info.
drupal_alter('entity_info', $entity_info);
cache_set("entity_info:$langcode", $entity_info);
}
}
if (empty($entity_type)) {
return $entity_info;
}
elseif (isset($entity_info[$entity_type])) {
return $entity_info[$entity_type];
}
}
The section of code where cache file is create is this:
if (defined("CACHE_GET_ENTITY_INFO") && CACHE_GET_ENTITY_INFO && defined("CACHE_DIR")) {
$cacheFile = CACHE_DIR . "/entity_get_info.phps";
$cachedData = file_exists($cacheFile) ? file_get_contents($cacheFile) : false;
if(! $cachedData) {
$entity_info = module_invoke_all('entity_info');
file_put_contents($cacheFile, serialize($entity_info));
} else {
$entity_info = unserialize($cachedData);
}
} else {
$entity_info = module_invoke_all('entity_info');
}
The constant "CACHE_DIR" is a constant that refer to cache directory in my solution. Make the adjustments for this as necessary.
Then the cleanup if the cache file:
function entity_info_cache_clear() {
drupal_static_reset('entity_get_info');
// Clear all languages.
cache_clear_all('entity_info:', 'cache', TRUE);
//Gilad: clearing the cached data of entity info
if (defined("CACHE_GET_ENTITY_INFO") && CACHE_GET_ENTITY_INFO && defined("CACHE_DIR")) {
$cacheFile = CACHE_DIR . "/entity_get_info.phps";
if (file_exists(CACHE_DIR . "/entity_get_info.phps")) {
unlink($cacheFile);
}
}
}
Thoughts for future...
Taking this solution for the next level would be adding caching in the level of **function module_invoke_all($hook) **
One of the great features of Durpal is the flexibility provided by allowing hooks to extend the CMS logic. However the same feature may be a major cause for performance issue. Basically if you have an algorithm that build setting, but running this algorithm again and again for each request produces the same result, one may want to consider caching the result produced by algorithm and serve it from file.
Therefore adding an ability to cache the result produced by running hooks on many modules cab speed up business logic dramatically.
Here is a conceptual solution:
function module_invoke_all($hook) {
$args = func_get_args();
// Remove $hook from the arguments.
unset($args[0]);
$return = array();
$doCaching = $is_hook_result_to_be_cached($hook);
if($doCaching) {
$cacheFile = CACHE_DIR . "/{$hook}_cache.phps";
$cachedData = file_exists($cacheFile) ? file_get_contents($cacheFile) : false;
}
if( $cachedData) {
return unserialize($cachedData);
}
foreach (module_implements($hook) as $module) {
$function = $module . '_' . $hook;
if (function_exists($function)) {
$result = call_user_func_array($function, $args);
if (isset($result) && is_array($result)) {
$return = array_merge_recursive($return, $result);
}
elseif (isset($result)) {
$return[] = $result;
}
}
}
if($doCaching) {
$cacheFile = CACHE_DIR . "/{$hook}_cache.phps";
file_put_contents($cacheFile, serialize($entity_info));
}
return $return;
}
Of course the function $is_hook_result_to_be_cached($hook) need to be created and defined, maybe involve some settings level in the admin section.
P.S. consider that all hosts are running today with electronic discs (and P.C uses SSD), therefore using file caching is a good and available solution. Of course other ways for caching can also be considered.