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
| 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. |
| Comment | File | Size | Author |
|---|---|---|---|
| #25 | 2531678-25.patch | 10.61 KB | mondrake |
Comments
Comment #1
mondrakeGood 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.
Comment #2
mondrakeWith tests. Test only patch == interdiff.
Comment #4
mondrakeSmall cleanup.
Comment #5
MattA commentedHmm, 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.
Comment #6
MattA commentedComment #7
mondrakeFixed the rotate issue and added additional tests to check that any operation that replaces the original resource is actually destroying the original.
Comment #8
mondrake@MattA re #5
Yeah that was my original thinking too. Then I changed my mind because:
ImageInterface::createNewmethod. It would mean an API change if we were to include a$is_tempargument 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.Comment #10
mondrakeFix patch in #7 is green.
Comment #11
mondrakeComment #12
MattA commentedActually, 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.
Comment #13
MattA commentedComment #14
mondrakeSo 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?
Comment #15
mondrakeOK, implemented #14.
Comment #16
MattA commentedComment #19
MattA commentedThe 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.
Comment #20
claudiu.cristeaShould wrap at 80 chars.
You can avoid the assertion messages.
The capitalization for CreateNew seems wrong. Maybe
ImageInterface::createNew()or, simply, the ID:'create_new'?Overall looks OK to me.
Comment #21
mondrakeAddressed all the comments in #20.
Comment #22
MattA commentedIn 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.
This test still needs to be moved to
testResourceDestruction()as per #19.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.Comment #23
mondrake#22 thanks, on it
Comment #24
mondrake#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.
Comment #25
mondrakeWhoops, missed to destroy the temporary resource.
Comment #28
MattA commentedYep, 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...
Comment #29
mondrake#28 LOL :)
Comment #30
mondrakeI 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?
Comment #31
fabianx commentedLooks good to me, but needs a change record.
Comment #32
mondrakeAdded draft CR https://www.drupal.org/node/2579991
Comment #33
mondrakeComment #34
mondrakeComment #35
catchThis could go into a patch release afaict, hence not 'rc deadline'.
Comment #36
alexpottI 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!