10

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:

screenshot

4
  • You can't "file_scan_directory" the "front page", as the front page is an entry in a database table, not a file-system path.
    – Letharion
    Commented Mar 1, 2012 at 12:19
  • @Letharion I think you have misunderstood my question. I mean the time this function takes when front page loads.I edited the question.
    – hknik
    Commented Mar 1, 2012 at 12:21
  • Does the front page really have anything to do with the function taking a specific amount of time? What directory are you actually scanning? What's in the directory?
    – Letharion
    Commented Mar 1, 2012 at 12:25
  • Aha! I thought you called the function yourself, and wondered why you didn't provide more details. Berdir's answer looks very reasonable. :)
    – Letharion
    Commented Mar 1, 2012 at 12:34

4 Answers 4

14

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.

7
  • No. Unfortunately(!) no module is missing from the filesystem
    – hknik
    Commented Mar 1, 2012 at 12:51
  • Then you need trace where the call is coming from, maybe a custom or contrib module is doing something wrong. Click through the parent functions of file_scan_directory() and update the initial post with the list of parent functions.
    – Berdir
    Commented Mar 1, 2012 at 12:55
  • 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: api.drupal.org/api/drupal/includes!bootstrap.inc/function/…. It does call the 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 function it isn't finding.
    – Berdir
    Commented Mar 1, 2012 at 13:14
  • @Berdir Your last comment should be in your answer, as it is relevant.
    – avpaderno
    Commented Mar 1, 2012 at 20:27
  • The links to "known issue in Drupal 7" and "Avoid rescanning module directory" are broken. Both are former stackexchange answers. Does anybody have other references?
    – rfay
    Commented Sep 17, 2015 at 19:42
4

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.

Modules removed from code base

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.

Coding errors

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.

2

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

0

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.