Using xhprof I noticed that file_scan_directory()
takes more than 10 seconds to execute when front page loads. Why should it take such a long time?
This is the output of xhprofile:
Using xhprof I noticed that file_scan_directory()
takes more than 10 seconds to execute when front page loads. Why should it take such a long time?
This is the output of xhprofile:
It sounds like you're affected by a known issue in Drupal 7.
Most probably, you are hitting Avoid re-scanning module directory when multiple modules are missing. It happens if you have some missing modules in your installation. Try to check your system table:
SELECT name, filename FROM system WHERE type = 'module' AND status = 1 ORDER BY filename
And clean-up any modules that are still enabled but missing from the filesystem.
Overall, Drupal 7 is way more resource friendly and scalable then Drupal 6, other than some unfortunate regressions like this one.
Looking at those functions, it does look like it's missing a module or maybe a single file of a module. Have a look at drupal_get_filename(), it does call the drupal_system_listing(), which calls this function if it can't find the requested file. Add a dpm(func_get_args()) right before it calls drupal_system_listing(), that should tell you which file it isn't finding.
There are several reasons why this issue may arise, and to my great dismay, I now find myself somewhat knowledgeable about those reasons. Frustratingly, if you've just noticed this problem after upgrading Drupal core to 7.33+, this could be a typo in any module, even if you haven't upgraded that module.
You may wish to first check for the known bug that @Berdir mentions, especially if you've recently been removing "unused" modules from the code base. To find out if you have modules that are enabled but have been removed from the file system you can run a script such as the one mentioned here - or use mine, written for a multi-site install on a system with drush, to be run from the Drupal base directory:
find sites -maxdepth 1 -iname '*.*' -type d | sed -rne 's:sites/(.+):echo \1; drush @\1 sqlq "select filename from system where status = 1" | grep "/" | sed -rne "s_(.+)_test -f \\1 || echo \\1_p" | bash:p' | bash
or the following:
while read -r file; do [ -f "$file" ] || echo "$file is missing."; done < <(drush sqlq "SELECT filename FROM system WHERE status = 1")
If you find a module that has been removed from the code base, follow the directions in the issues that @Berdir mentioned.
If it's not that, your situation is likely caused by a coding error, such as a file that has been removed but is still being added by a drupal_add_js call (from comment 19 in issue #1082892) or an unfortunate typo in a module or theme, e.g. imagecache_actions
(see https://drupal.org/node/2381357).
In any case, to figure out exactly why this is happening, you need to know exactly which file Drupal can't find. So, as per Berdir's comment, you can temporarily hack drupal_get_filename
in bootstrap.inc
by adding a log or message call just before the call to drupal_system_listing()
. If you have Devel module installed then dpm
will work; if not, you can use drupal_set_message
or syslog. Examples:
dpm(func_get_args());
drupal_set_message(implode(', ', func_get_args()));
syslog(LOG_WARNING, implode(', ', func_get_args()));
Once you know what Drupal is looking for, it's a good bet that you'll be able to figure out where to go from there. My problem was caused by a call to include a file from the non-existent module imagcache_actions
(note the typo). So, I searched for imagecache_actions
in my codebase (e.g. grep -r imagcache_actions .
), and found that version 1.4 of imagecache_canvasactions.module
uses module_load_include outside of any function call, in the file scope, with a typo. Again, this error was only exposed after updating to Drupal 7.33+. I found that an issue had already been created for imagecache_actions
, applied the patch, and was back in business.
I had a very similar issue - file_scan_directory()
was killing the site. Turns out a huuge node_modules
folder embedded within my custom theme for gulp
was being scanned each cache flush. Moving these files out of the theme folder (and updating some paths in my gulpfile) seemed to fix it for me. Alternatively: I think you can hack file.inc
:
'nomask' => '/(\.\.?|CVS|node_modules)$/', // https://www.drupal.org/node/2329453#comment-9360519
The file_scan_directory()
is a recursive function which all files that match for a given directory. It's uses is_dir()
and opendir()
PHP calls which may be most time expensive in terms of I/O system calls. Simple Drupal bootstrap (e.g. time drush ev ""
) may call file_scan_directory
over few thousands times (depending on complexity of your Drupal folder hierarchy, e.g. number of modules and its folders).
In my case I had ~1500 calls to file_scan_directory
(24 seconds in total consisting 2 calls from drupal_system_listing
in common.inc
, then the other calls were split by recursive calls to file_scan_directory
it-self.
In order to improve the performance on I/O calls, you need to implement the file caching. This can be achieved by installing and enabling OPCache (opcache.enable=1
) and tweak its settings (see: How to use PHP OPCache?). Using memory-based caching such as memcached/redis is also advised.
When using command-line interface (such as drush
), you should also enable opcache.enable_cli=1
.
After the change you can check the more consuming syscalls using some available debuggers.
E.g.
On Linux using strace
(hit Ctrl-C to finish):
sudo strace -c -fp $(pgrep -n php)
On Unix using dtrace
(using PHP's DTrace static probes), e.g.
sudo dtrace -n 'inline string NAME = "php"; syscall:::entry /(NAME == strstr(NAME, execname)) || (execname == strstr(execname, NAME))/ { @num[probefunc] = count(); }'
You can further consider optimizing drupal_system_listing()
or file_scan_directory()
by implementing static cache, e.g.
--- a/includes/file.inc
+++ b/includes/file.inc
@@ -2104,6 +2104,8 @@ function file_download_access($uri) {
* 'filename', and 'name' members corresponding to the matching files.
*/
function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
+ static $dirs = array();
+
// Merge in defaults.
$options += array(
'nomask' => '/(\.\.?|CVS)$/',
@@ -2120,7 +2122,12 @@ function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
if (!preg_match($options['nomask'], $filename) && $filename[0] != '.') {
$uri = "$dir/$filename";
$uri = file_stream_wrapper_uri_normalize($uri);
- if (is_dir($uri) && $options['recurse']) {
+
+ if (empty($dirs[$uri])) {
+ $dirs[$uri] = is_dir($uri);
+ }
+
+ if ($dirs[$uri] && $options['recurse']) {
// Give priority to files in this folder by merging them in after any subdirectory files.
$files = array_merge(file_scan_directory($uri, $mask, $options, $depth + 1), $files);
Or for caching file_scan_directory
calls from drupal_system_listing()
, then check the following patch available at: file_scan_directory should be cached.