image
image copied to clipboard
Image::encode() leaks memory
Hey, I'm working on a batch script that processes lots of images in a loop, always crashing due to memory exhaustion caused by intervention/image. Here is a very simplified script that reproduces this problem:
use Intervention\Image\ImageManager;
$manager = new ImageManager(array('driver' => 'gd'));
for ($i = 0; $i < 1000; $i++) {
$image = $manager->make(__DIR__ . '/image.png');
$image->encode('jpg');
echo "$i. " . memory_get_peak_usage(true) . "\n";
}
I've traced this problem to this assignment https://github.com/Intervention/image/blob/master/src/Intervention/Image/AbstractEncoder.php#L87, which I believe creates a circular reference between the Image and Encoder instances, causing the memory to never be freed.
Simply unsetting the $this->image
reference after the image is encoded fixes this problem.
public function process(Image $image, $format = null, $quality = null)
{
...
$this->setImage(null);
return $image->setEncoded($this->result);
}
I would use destroy() to free memory in each iteration. Unsetting the image after encoding, might not be always wanted.
That doesn't fix the problem, it still leaks, just a lot less. Better workaround is to call gc_collect_cycles()
in each iteration, it's still just a workaround though, end user shouldn't be expected to do this.
So this seems to be a problem of the GD function. Or is it the code from Intervention Image leaking memory?
This is an issue with Intervention Image itself (it can be reproduced with both gd and imagick), I believe what is happening is:
- on
Image::make
call anImage
andDriver
instances are created and reference to aDriver
instance is stored asImage::$driver
- new
Encoder
instance is created in theDriver
constructor and stored asDriver::$encoder
- on
Image::encode
call theImage
instance is passed toEncoder::process
, where it is stored asEncoder::$image
(I guess this is done for convenience, so theImage
instance doesn't have to be passed as argument all over the place) - now when I'm done with saving the encoded image and I unset the
Image
instance in my client-code (and I expect the memory to be freed at that point) a reference to theImage
instance still exists inEncoder::$image
, which itself isn't freed because it's referenced inDriver::$encoder
which is referenced inImage::$driver
- basically creating circular reference between theImage
andEncoder
instances
That's why unsetting the Image
reference in Encoder::process
after it's no longer needed fixes the problem. Unsetting Image
reference in my client-code in that case frees the memory as no other references for that object exist at that point.
I hope that makes at least some sense, I'm pretty awful at explaining stuff. :)
+1
I can confirm that $this->setImage(null);
fixes also the problem for Imagick.
Test case using this picture (on the filesystem):
require 'vendor/autoload.php';
use Intervention\Image\ImageManager;
$manager = new ImageManager(array('driver' => 'imagick'));
for ($i = 0; $i < 10; $i++) {
$image = $manager->make(__DIR__ . '/lichtenstein.jpg');
$image->encode('png');
$image->destroy();
echo "$i. " . memory_get_peak_usage(true) . "\n";
}
Output without $this->setImage(null);
:
0. 6553600
- 11796480
- 17039360
- 22282240
- 27525120
- 32768000
- 38010880
- 43253760
- 48496640
- 53739520
Output with $this->setImage(null);
:
0. 6553600
- 7077888
- 7077888
- 7077888
- 7077888
- 7077888
- 7077888
- 7077888
- 7077888
- 7077888
But it still takes 20.45 seconds to process the whole script using the Imagick driver and 16.75 seconds using the GD driver.
Server details: Intel Xeon E3-1225 V2 @ 3.20GHz - 16GB Ram ImageMagick 6.9.2-10 Q16 x86_64 2015-12-26 GD 2.1.1 PHP Version 5.6.16
Can this also be fixed on 2.2.2? It's the latest version for PHP 5.3, which we use.
Ran into this when trying to batch optimize a bunch of jpegs today... $this->setImage(null);
worked wonders.