#๐Ÿ”’ Is caching opened image file objects a bad practice?

28 messages ยท Page 1 of 1 (latest)

upbeat hamlet
#

I have a Discord bot that does image generation using the PIL library.
To save image opening time, I store the opened image file in memory with the help of cachetools.TTLCache.
But after doing so, I've been seeing some errors, such as file pointer issues (like reading the image while caching at the same time? not exactly sure what happened). So I added a threading.Lock() to the lock parameter of TTLCache.
And now, I see a new error that sometimes occur when opening images, where the file pointer will become NoneType, I asked gpt and it says it might be my caching system that's causing the issue.
So I'm thinking that, maybe I am on the wrong track? Am I not supposed to cache opend image objects?

sleek marlinBOT
#

@upbeat hamlet

Python help channel opened

Remember to:

  • Ask your Python question, not if you can ask or if there's an expert who can help.
  • Show a code sample as text (rather than a screenshot) and the error message, if you've got one.
  • Explain what you expect to happen and what actually happens.

:warning: Do not pip install anything that isn't related to your question, especially if asked to over DMs.

upbeat hamlet
#

code of caching implementation

deco = cachetools.cached(cachetools.TTLCache(maxsize=128, ttl=300), lock=threading.Lock())
image_read = deco(Image.open)
def open_image(file_path: pathlib.Path | str, size: tuple[int, int] | None = None) -> Image.Image:
    image = image_read(file_path)
    image = image.convert("RGBA")

    if size is not None:
        image = image.resize(size, Image.Resampling.LANCZOS)
    return image.copy()
fallen plinth
upbeat hamlet
#

seems to be true...

from PIL import Image
import time

for _ in range(5):
    start = time.perf_counter()
    Image.open("hoyo_buddy/web_app/assets/images/dev_tools_tutorial.gif")
    print(time.perf_counter() - start)
0.02123070000379812
0.0002891999974963255
0.0002630000017234124
0.00019439999596215785
0.00024839999969117343
acoustic flint
#

this documentation seems relevant as well:
https://pillow.readthedocs.io/en/stable/reference/open_files.html#image-lifecycle

When the pixel data from the image is required, Image.load() is called. The current frame is read into memory. The image can now be used independently of the underlying image file.

The lifecycle of a single-frame image is relatively simple. The file must remain open until the load() or close() function is called or the context manager exits.

upbeat hamlet
#

i've never used the load() or close() methods tho, i only use Image.open

acoustic flint
#

i'd assume once you've called load(), you've saved the image in memory and can make as many copies of it as you want

upbeat hamlet
#

so it only closes the image when close() is called? (or when it is gced)

#

by closes the image i mean offloading it from mem

acoustic flint
#

close() is different from the context manager in that not only will it close the file, but also clean up the image data from memory

upbeat hamlet
#

hmm, i see

acoustic flint
#

as per that same image lifecycle doc:

Image.Image.close() Closes the file and destroys the core image object.

The Pillow context manager will also close the file, but will not destroy the core image object. e.g.:

with Image.open("test.jpg") as img:
    img.load()
assert img.fp is None
img.save("test.png")
upbeat hamlet
#

but what i want to do here is let the image file retain longer in the memory (around 5 minutes) so that future image opening of the same file can take the opened image file directly from mem (faster)

fallen plinth
#

I would try something like ```py
@cachetools.cached(cachetools.TTLCache(maxsize=128, ttl=300), lock=threading.Lock())
def image_read(path):
with Image.open("test.jpg") as img:
img.load()
return img.copy()

#

not sure if that'll work though, specially if you are dealing with gifs

upbeat hamlet
#

๐Ÿ˜”

#

ok, thanks for your help!

pulsar lichen
#

I would expect that all you need is to add a .load() call into your open_image(). And maybe don't do copy()?

#

in theory after a load it should be in memory.

upbeat hamlet
pulsar lichen
#

interestingly:

The lock context manager is used only to guard access to the cache object. The underlying wrapped function will be called outside the with statement, and must be thread-safe by itself.
so currently, you can potentially get two concurrent image reads. this is probably only a problem if they're to the same file, though, and maybe not even then

upbeat hamlet
#

yeah, i see why (the error happend) now

#

i'm gonna remove the caching implementation for now, the performance benefit it brings doesn't match up with the trouble it can potentially have

#

i prefer reliability

#

thanks for the helps

sleek marlinBOT
#
Python help channel closed

This help channel has been closed and it's no longer possible to send messages here. If your question wasn't answered, feel free to create a new post in #1035199133436354600. To maximize your chances of getting a response, check out this guide on asking good questions.