Open TTMaDe opened 5 months ago
Pillow's memory allocator doesn't necessarily release the memory in the pool back as soon as an image is destroyed, as it uses that memory pool for future allocations. See Storage.c (https://github.com/python-pillow/Pillow/blob/main/src/libImaging/Storage.c#L310) for the implementation.
If you repeatedly open and close an image, you should not see the memory increase, but it won't necessarily drop between destruction and allocation again.
(edit: related: #5401, #3610)
It looks like it caches 0 blocks by default though.
And you can set the number of blocks to cache with the PILLOW_BLOCKS_MAX
environment variable.
There's a docs page for this actually: https://pillow.readthedocs.io/en/stable/reference/block_allocator.html
Thanks for the quick answers!
Indeed, when I set the PILLOW_BLOCKS_MAX=5
environment variable the used memory decreases when releasing/closing the images. But after reading the linked docs page, I would expect that if I manually set the environment variable to 0 (or just leave it unset), the memory pools will be disabled, no block caching will occur and the memory of closed images will be freed immediately.
But with this default settings our application ran out of memory on a 16 GB Linux system after reading, modifying and closing images in a loop.
This may or may not help - in your original code you open an image and don't close it. It is recommended instead that you either call img.close()
when you are done or use a context manager for the image. See https://pillow.readthedocs.io/en/stable/deprecations.html#image-del and https://github.com/python-pillow/Pillow/blob/e8ab5640774716c5486d3cb05167f74f742ad6ef/src/PIL/Image.py#L560-L565
Edit: I see you've mentioned 'closing images' in your comments, so this remark can just be for reference to others.
I didn't catch this before but what you're doing is basically opening 50 copies of an image and keeping them all.
Can you show us a flow where you expect constant memory usage?
Yes, in the first loop I open the image 50 times and hold 50 copies so the memory usage increases which is ok.
In the second loop, I delete a container containing an image copy in each iteration, so I would expect memory usage to decrease after each iteration. But the used memory only decreased if I manually set PILLOW_BLOCKS_MAX
to a value > 0.
Manually closing the image copy via self.value.close()
in the destructor of the Container class doesn't make a difference so I removed it:
import gc
import os
import sys
import time
import PIL.Image
import psutil
FILE = './test_image.png'
def LogMemory():
pid = os.getpid()
rss = 0
for mmap in psutil.Process(pid).memory_maps():
# All memory that this process holds in RAM. RSS = USS + Shared.
rss += mmap.rss
return rss
class Container:
def __init__(self):
self.value = None
def SetValue(self, value):
self.value = self._copyValue(value)
def GetValue(self):
return self._copyValue(self.value)
def _copyValue(self, value):
return value.copy()
if __name__ == '__main__':
print(f'Using Python {sys.version}, PIL {PIL.__version__}')
print(f'PILLOW_ALIGNMENT: {PIL.Image.core.get_alignment()}')
print(f'PILLOW_BLOCK_SIZE: {PIL.Image.core.get_block_size()}')
print(f'PILLOW_BLOCKS_MAX: {PIL.Image.core.get_blocks_max()}')
containers = []
for i in range(50):
before = LogMemory()
img = PIL.Image.open(FILE)
# img = [1] * (1920*1080*3) # this works!
container = Container()
container.SetValue(img)
containers.append(container)
after = LogMemory()
print(f'Loaded image {i} took {after-before} bytes')
for i in range(len(containers)):
before = LogMemory()
containers.pop()
time.sleep(0.1)
after = LogMemory()
print(f'popped container {i} released {before-after} bytes')
print('Delete list')
before = LogMemory()
containers = None
gc.collect()
after = LogMemory()
print(f'Finally released {before-after} bytes')
Running the code with PILLOW_BLOCKS_MAX=1
prints:
Using Python 3.11.8 (main, Feb 25 2024, 16:41:26) [GCC 9.4.0], PIL 10.2.0
PILLOW_ALIGNMENT: 1
PILLOW_BLOCK_SIZE: 16777216
PILLOW_BLOCKS_MAX: 1
Loaded image 0 took 17731584 bytes
Loaded image 1 took 8339456 bytes
Loaded image 2 took 8298496 bytes
Loaded image 3 took 8298496 bytes
...
Loaded image 49 took 8298496 bytes
popped container 0 released -12288 bytes
popped container 1 released 0 bytes
popped container 2 released 8298496 bytes
popped container 3 released 8298496 bytes
popped container 4 released 8298496 bytes
popped container 5 released 8298496 bytes
popped container 6 released 8298496 bytes
popped container 7 released 8298496 bytes
...
popped container 48 released 8298496 bytes
popped container 49 released 8298496 bytes
Delete list
Finally released 0 bytes
The memory usage decreases with every containers.pop()
But when I run with PILLOW_BLOCKS_MAX=0
or just leave the environment variable unset I get:
Using Python 3.11.8 (main, Feb 25 2024, 16:41:26) [GCC 9.4.0], PIL 10.2.0
PILLOW_ALIGNMENT: 1
PILLOW_BLOCK_SIZE: 16777216
PILLOW_BLOCKS_MAX: 0
Loaded image 0 took 17735680 bytes
Loaded image 1 took 8114176 bytes
Loaded image 2 took 8069120 bytes
Loaded image 3 took 8036352 bytes
Loaded image 4 took 8044544 bytes
Loaded image 5 took 8052736 bytes
Loaded image 6 took 8052736 bytes
...
Loaded image 48 took 8052736 bytes
Loaded image 49 took 8065024 bytes
popped container 0 released 0 bytes
popped container 1 released 0 bytes
popped container 2 released 0 bytes
popped container 3 released 0 bytes
popped container 4 released 0 bytes
popped container 5 released 0 bytes
popped container 6 released 0 bytes
popped container 7 released 0 bytes
...
popped container 48 released 0 bytes
popped container 49 released 8298496 bytes
Delete list
Finally released 0 bytes
and the used memory doesn't decrease while popping the containers from the list.
So setting PILLOW_BLOCKS_MAX
to a value > 0 fixes my problem because the memory is freed but after reading the linked doc I would expect setting PILLOW_BLOCKS_MAX
to 0 disables caches and memory will also be freed on each iteration.
What did you do?
Our application works with PIL images and holds a list of containers. Every container has a copy of the last image to track manipulations of the image data. When we delete the containers, the memory reserved by the PIL images is not released. Even closing the image manually via
image.close()
in the container's desctructor and calling the garbage collector does not release the memory.If I replace the PIL image with a Python list (line: 55) the memory gets freed when a container is popped from the list.
What did you expect to happen?
The memory, taken by a PIL image copy, should be released after each
containers.pop()
.What actually happened?
The memory isn't released.
Script output
```text Using Python 3.11.8 (main, Feb 25 2024, 16:41:26) [GCC 9.4.0], PIL 10.3.0 Load image 0 RSS: 21696512 RSS: 40095744 Load image 1 RSS: 40108032 RSS: 48123904 Load image 2 RSS: 48123904 RSS: 56512512 Load image 3 RSS: 56512512 RSS: 64679936 Load image 4 RSS: 64679936 RSS: 72974336 Load image 5 RSS: 72974336 RSS: 81268736 Load image 6 RSS: 81268736 RSS: 89563136 Load image 7 RSS: 89563136 RSS: 97857536 Load image 8 RSS: 97857536 RSS: 106151936 Load image 9 RSS: 106151936 RSS: 114446336 Load image 10 RSS: 114446336 RSS: 122740736 Load image 11 RSS: 122740736 RSS: 131035136 Load image 12 RSS: 131035136 RSS: 139329536 Load image 13 RSS: 139329536 RSS: 147623936 Load image 14 RSS: 147623936 RSS: 155918336 Load image 15 RSS: 155918336 RSS: 164212736 Load image 16 RSS: 164212736 RSS: 172507136 Load image 17 RSS: 172507136 RSS: 180801536 Load image 18 RSS: 180801536 RSS: 189095936 Load image 19 RSS: 189095936 RSS: 197390336 Load image 20 RSS: 197390336 RSS: 205684736 Load image 21 RSS: 205684736 RSS: 213979136 Load image 22 RSS: 213979136 RSS: 222273536 Load image 23 RSS: 222273536 RSS: 230576128 Load image 24 RSS: 230576128 RSS: 238964736 Load image 25 RSS: 238964736 RSS: 247156736 Load image 26 RSS: 247156736 RSS: 255451136 Load image 27 RSS: 255451136 RSS: 263745536 Load image 28 RSS: 263745536 RSS: 272039936 Load image 29 RSS: 272039936 RSS: 280334336 Load image 30 RSS: 280334336 RSS: 288628736 Load image 31 RSS: 288628736 RSS: 296923136 Load image 32 RSS: 296923136 RSS: 305217536 Load image 33 RSS: 305217536 RSS: 313511936 Load image 34 RSS: 313511936 RSS: 321806336 Load image 35 RSS: 321806336 RSS: 330100736 Load image 36 RSS: 330100736 RSS: 338395136 Load image 37 RSS: 338395136 RSS: 346689536 Load image 38 RSS: 346689536 RSS: 354983936 Load image 39 RSS: 354983936 RSS: 363278336 Load image 40 RSS: 363278336 RSS: 371572736 Load image 41 RSS: 371572736 RSS: 379867136 Load image 42 RSS: 379867136 RSS: 388161536 Load image 43 RSS: 388161536 RSS: 396455936 Load image 44 RSS: 396455936 RSS: 404750336 Load image 45 RSS: 404750336 RSS: 413044736 Load image 46 RSS: 413044736 RSS: 421416960 Load image 47 RSS: 421416960 RSS: 429633536 Load image 48 RSS: 429633536 RSS: 437927936 Load image 49 RSS: 437927936 RSS: 446222336 pop container 0 RSS: 446222336 RSS: 446222336 pop container 1 RSS: 446222336 RSS: 446222336 closed image in destructor RSS: 446222336 RSS: 446226432 pop container 2 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 3 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 4 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 5 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 6 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 7 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 8 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 9 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 10 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 11 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 12 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 13 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 14 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 15 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 16 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 17 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 18 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 19 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 20 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 21 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 22 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 23 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 24 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 25 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 26 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 27 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 28 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 29 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 30 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 31 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 32 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 33 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 34 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 35 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 36 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 37 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 38 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 39 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 40 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 41 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 42 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 43 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 44 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 45 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 46 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 47 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 48 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 446226432 RSS: 446226432 pop container 49 RSS: 446226432 RSS: 446226432 closed image in destructor RSS: 437927936 RSS: 437927936 Delete list RSS: 437927936 RSS: 437927936 ```What are your OS, Python and Pillow versions?