Problem/Motivation

The current implementations of the 'create_new' and 'rotate' GD image operations do not free memory from the original resource before replacing the reference. This bug is mitigated by the fact that the image object must be loaded at least once, prior to using the operation.

Sample code:

$image = \Drupal::service('image.factory')->get("path/to/image.png");
// Any operation that uses the resource will load it into memory.
$image->getToolkit()->getResource();
// This replaces the old resource without freeing it first.
$image->createNew($width, $height);

Proposed resolution

Add an 'is_temp' argument to the 'create_new' operation and call imagedestroy() on the original resource if it is FALSE.

Known workarounds: Developers can get the reference prior to using 'create_new' and manually free the memory (assuming you know the toolkit in use).

Remaining tasks

Review.

User interface changes

None.

API changes

For image objects, the createNew() function is not changed. For GD image operation plugins that use temporary images, 'is_temp' must now be set to TRUE.

Data model changes

None.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because some GD operations are failing to manage properly their memory usage.
Issue priority Normal because the impact is only evident in case of massive operations on a number of images which core is not doing, but in contrib this may have a bigger impact.
Prioritized changes The main goal of this issue is to reduce fragility.
Disruption Minimal, adds an optional argument that only impacts developers creating new GD operation plugins.

Comments

mondrake’s picture

Status: Active » Needs review
Issue tags: +Needs tests
StatusFileSize
new11.77 KB

Good catch. The problem with CreateNew is that it is not just used to erase the current image and create a totally new one - it is also needed by other operations to create a temporary resource for copying between the original resource and the temp one, drop the old one, and finally set the temp one as current.
But, true, in case of 'totally new image' that leaves an orphaned resource around ATM.

This patch introduces a new toolkit 'create_temp' operation (in fact it's the same code as the current 'create_new'), only specific for the GD toolkit, that is not exposed to the ImageInterface, but only meant to be used by the other GD operations (including 'create_new'). The operations then need to manage what to do with the old resource.

Needs some tests.

mondrake’s picture

Issue tags: -Needs tests
StatusFileSize
new949 bytes
new12.7 KB

With tests. Test only patch == interdiff.

The last submitted patch, 2: 2531678-2-test-only.patch, failed testing.

mondrake’s picture

StatusFileSize
new12.91 KB
new3.02 KB

Small cleanup.

MattA’s picture

Status: Needs review » Needs work

Hmm, not sure if a new 'create_temp' operation is the right way to go, can't we just add an 'is_temp' argument to the 'create_new' operation? Assuming it defaults to FALSE, any other operations using it for a temporary image would have to pass in TRUE and we would know that they mean to manually destroy the original.

On a slightly related note, I was also looking at some other operations, seems rotate also fails to destroy the original.

MattA’s picture

Title: The 'create_new' image operation does not release memory » The 'create_new' and 'rotate' image operations do not release memory
mondrake’s picture

Status: Needs work » Needs review
StatusFileSize
new15.88 KB
new2.17 KB
new1.72 KB
new1.06 KB

Fixed the rotate issue and added additional tests to check that any operation that replaces the original resource is actually destroying the original.

mondrake’s picture

@MattA re #5

... can't we just add an 'is_temp' argument to the 'create_new' operation...

Yeah that was my original thinking too. Then I changed my mind because:

  1. The 'create_new' operation is also wrapped by the ImageInterface::createNew method. It would mean an API change if we were to include a $is_temp argument to the interface. Besides, this is GD specific - in GD we need temporary resources to copy images. But this would be alien to a toolkit like e.g. Imagemagick. Therefore I am reluctant to add a GD specific parameter to a toolkit-agnostic interface.
  2. Implementers of other toolkits will likely just copy/paste the GD 'create_new' operation code to start their toolkits - I though to separate clearly the operation of getting a temp resource (GD specific) from actually creating a brand new image (non GD specific).

Status: Needs review » Needs work

The last submitted patch, 7: 2531678-7-test-only.patch, failed testing.

mondrake’s picture

Status: Needs work » Needs review

Fix patch in #7 is green.

mondrake’s picture

Issue summary: View changes
MattA’s picture

Actually, it wouldn't be an API change at all. ImageInterface defines functions for image objects, and has no bearing on image operation plugins. Since its createNew() function should never create a temporary image, adding the parameter wouldn't make sense either. Therefore, the only users of the argument would be other GD operations.

MattA’s picture

Status: Needs review » Needs work
mondrake’s picture

So your proposal is to add the 'is_temp' argument to the plugin, without adding a corresponding argument to the createNew() method of ImageInterface?

Could work. Is there any other opinion out there?

mondrake’s picture

Status: Needs work » Needs review
StatusFileSize
new8.93 KB
new13.08 KB

OK, implemented #14.

MattA’s picture

Issue summary: View changes
StatusFileSize
new9.08 KB
new2.26 KB
  • Updated the argument description to a natural language sentence.
  • Removed the test for-loop as it wasn't necessary and reduced the size of the new image so that we don't waste so much memory during SimpleTest runs.

mondrake queued 16: 2531678-16.patch for re-testing.

mondrake queued 16: 2531678-16.patch for re-testing.

MattA’s picture

The tests should probably be moved to the new testResourceDestruction() that was added in #2531570.

Also, since this probably won't go in anytime soon, we may need to go with plan B and use an updated version of #7 instead.

claudiu.cristea’s picture

Status: Needs review » Needs work
Issue tags: +Performance, +memory
  1. #19 is not addressed and I find that correct.
  2. +++ b/core/modules/system/src/Plugin/ImageToolkit/Operation/gd/Rotate.php
    @@ -98,17 +98,25 @@ protected function execute(array $arguments) {
    +      // GIFs need to reassign the transparent color after performing the rotate,
    +      // but only do so, if the image already had transparency of its own, or the
    +      // rotate added a transparent background.
    

    Should wrap at 80 chars.

  3. +++ b/core/modules/system/src/Tests/Image/ToolkitGdTest.php
    @@ -278,9 +278,19 @@ function testManipulations() {
    +          $this->assertFalse(is_resource($old_res), SafeMarkup::format("'%operation' destroyed the original resource.", ['%operation' => $values['function']]));
    
    @@ -389,6 +399,15 @@ function testManipulations() {
    +    $this->assertTrue(is_resource($old_res), 'Successfully loaded an existing image.');
    ...
    +    $this->assertFalse(is_resource($old_res), 'CreateNew destroyed the original resource.');
    +    $this->assertTrue(is_resource($new_res), 'CreateNew has created a new resource.');
    

    You can avoid the assertion messages.

  4. +++ b/core/modules/system/src/Tests/Image/ToolkitGdTest.php
    @@ -389,6 +399,15 @@ function testManipulations() {
    +    // Test that CreateNew does not leave orphaned GD resources.
    

    The capitalization for CreateNew seems wrong. Maybe ImageInterface::createNew() or, simply, the ID: 'create_new'?

Overall looks OK to me.

mondrake’s picture

Status: Needs work » Needs review
StatusFileSize
new10.06 KB
new3.61 KB

Addressed all the comments in #20.

MattA’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/system/src/Tests/Image/ToolkitGdTest.php
    @@ -389,16 +399,28 @@ function testManipulations() {
    -    $this->assertFalse($image->isValid(), 'CreateNew with negative width fails.');
    +    $this->assertFalse($image->isValid(), 'create_new with negative width fails.');
    ...
    -    $this->assertFalse($image->isValid(), 'CreateNew with invalid extension fails.');
    +    $this->assertFalse($image->isValid(), 'create_new with invalid extension fails.');
    ...
    

    In the spirit of keeping the patch limited to only required changes, these can probably be left out.

    For what it's worth though, 'CreateNew' the class, 'create_new' the operation id, and 'createNew' the interface method all technically refer to the same thing and are capitalized correctly, so all are more or less acceptable.

  2. +++ b/core/modules/system/src/Tests/Image/ToolkitGdTest.php
    @@ -389,16 +399,28 @@ function testManipulations() {
    +    // Test that 'create_new' operation does not leave orphaned GD resources.
    +    $image = $this->imageFactory->get(drupal_get_path('module', 'simpletest') . '/files/image-test.png');
    +    $old_res = $image->getToolkit()->getResource();
    ...
    

    This test still needs to be moved to testResourceDestruction() as per #19.

  3. Upon further inspection, it seems that the implementations of crop and rotate have not completely solved the issue. If the call to imagecopyresampled() succeeds, then everything works as intended, but if it fails, the toolkit resource is left pointing to the blank temporary image, and the original resource is not destroyed. Ideally, upon failure, the temporary image should be freed from memory and the toolkit resource should be restored to the original image.
mondrake’s picture

Assigned: Unassigned » mondrake

#22 thanks, on it

mondrake’s picture

Assigned: mondrake » Unassigned
Status: Needs work » Needs review
StatusFileSize
new4.44 KB
new10.42 KB

#22.1 - reverted
#22.2 - done
#22.3 - fixed - but it was 'resize' and not 'rotate' that was missing restoring the original resource. 'rotate' seems ok to me.

mondrake’s picture

StatusFileSize
new1.46 KB
new10.61 KB

Whoops, missed to destroy the temporary resource.

Status: Needs review » Needs work

The last submitted patch, 25: 2531678-25.patch, failed testing.

Status: Needs work » Needs review

mondrake queued 25: 2531678-25.patch for re-testing.

MattA’s picture

Yep, that's my bad. I meant resize. Probably re-read the issue title and subconsciously decided to type that instead of what was open in my IDE...

mondrake’s picture

#28 LOL :)

mondrake’s picture

Priority: Normal » Major
Issue tags: +rc deadline

I would really like to see this in before RC, current workarounds are funky and this patch makes memory management just much better - what's missing to RTBC?

fabianx’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs change record

Looks good to me, but needs a change record.

mondrake’s picture

mondrake’s picture

Issue tags: -Needs change record
mondrake’s picture

catch’s picture

Issue tags: -rc deadline

This could go into a patch release afaict, hence not 'rc deadline'.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

I think cleaning up after ourselves is a good idea. The change is minimal and not disruptive or risky however it does impact performance of some of the most memory intensive operations we have and therefore worth doing. Committed 42da7fa and pushed to 8.0.x. Thanks!

  • alexpott committed 42da7fa on 8.0.x
    Issue #2531678 by mondrake, MattA: The 'create_new' and 'rotate' image...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.