Wednesday 29 July 2009

An easy way to leak memory using DirectoryIterator

I've been trying to track down a memory leak while running the PHP_CodeSniffer unit tests after being told the PEAR-wide test suite was running out during my run. My own testing showed the unit tests started at 11MB of memory used and ballooned out to about 56MB, even with no error messages being generated.

I already do a bit of cleanup to save memory (unset()ing some member vars mostly) and I tried cleaning just about everything else out, but memory usage didn't drop. I don't have any circular references, so I had nothing obvious to try.

After a bit of playing, I found the problem; a DirectoryIterator.

My code looked a bit like this:

$di = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
foreach ($di as $file) {
$basename = basename($file, '.php');
if (substr($basename, -5) !== 'Sniff') {
continue;
}
}

The $file variable is a DirectoryIterator item. I was passing it into the basename() function and relying on the fact that PHP would cast it to a string (the file name).

I changed the code to grab the file name first and then pass it into the basename() function, like this:
$di = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
foreach ($di as $file) {
$fileName = $file->getFilename();
$basename = basename($fileName, '.php');
if (substr($basename, -5) !== 'Sniff') {
continue;
}
}

So now the tests only use 16MB of memory; a healthy saving of 40MB. There is probably a bit more that can be saved by calling this code less during testing, but this was a good (and easy) result regardless.