Memory leak using wx.CallAfter with BytesIO object

I seem to be getting a memory leak when I pass a BytesIO object (containing and animated GIF) from a helper thread using wx.CallAfter to update a wx.AnimationCtrl in the main thread. I’ve posed some sample code that generates the problem. Everything works fine if I save the gif to a file, pass the file name to main as a string, and then update the animation control from the file. With a BytesIO object, the memory just continues to go up in several MB chunks with each update. I’ve also tried destroying and creating the animation control for each new update, to no effect. In the actual application I am downloading images and would like to do all the updates in memory rather than write files. I am doing this on a RaspberryPI 3 model B so there is not a lot of spare memory to play with.

Any thoughts on what I am doing wrong here?

type or paste code here

#!/usr/bin/env python

import wx
from wx.adv import Animation, AnimationCtrl
import threading
from PIL import Image
import io


class AnimatedGIFThread(threading.Thread):

    def __init__(self, window):
        threading.Thread.__init__(self)
        self._want_abort = 0
        self.window = window
        self.timeRefresh = threading.Event()
        self.timeRefresh.clear()
        self.timeDelay = 10   # Time (seconds) between updates.


    def run(self):
        images_lst = [None]*3
        duration_lst = [250]*3

        for i in range(3):
            fpath = '/home/pi/python/source/testing/testimage' + str(i) + '.png'
            images_lst[i] = Image.open(fpath)

        while True:  # Check for abort
            if self._want_abort:
                return

            animated_gif = self.BuildAnimatedGIF(images_lst, duration_lst)

            wx.CallAfter(self.window.UpdateGIF, animated_gif)

            self.timeRefresh.wait(self.timeDelay)  # Delay between updates


    def BuildAnimatedGIF(self, i_lst, d_lst):
        a_gif = io.BytesIO()
        a_gif.seek(0)
        
        i_lst[0].save(a_gif,
                      format='GIF',
                      save_all=True,
                      append_images=i_lst[1:],
                      duration=d_lst,
                      loop=0)
        
        a_gif.seek(0,2)
        print('GIF image size = ', a_gif.tell())

        return(a_gif)


    def abort(self):
        self._want_abort = True


#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

class MainFrame(wx.Frame):

    def __init__(self):

        # Frame with no border
        wx.Frame.__init__(self, None, title="Animated GIF Test", pos=(0,0), size=(460,460))

        # Create a panel
        self.panel = wx.Panel(self, -1)

        # Animated gif control
        self.agif = AnimationCtrl(self.panel,
                                  wx.ID_ANY,
                                  anim=wx.adv.NullAnimation,
                                  pos=(40,40),
                                  size=(420,360),
                                  style=wx.adv.AC_DEFAULT_STYLE)

        # Event bindings with no calling objects
        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)

        # Start thread
        self.thread_gif = AnimatedGIFThread(self)
        self.thread_gif.start()


    def OnCloseWindow(self, event):
        try:
            self.thread_gif.abort()
            self.thread_gif.stop()
        except: pass
            
        self.Destroy()


    def UpdateGIF(self, a_gif):
        a_gif.seek(0)
        self.agif.Load(a_gif)
        self.agif.Play()


if __name__ == '__main__':
    app = wx.App()
    frame = MainFrame()
    frame.Show(True)
    app.MainLoop()

I discovered a memory leak in a WxPython application and with a bit of patience, nailed it to BytesIO (by looking at reference counts). In my case, adding an explicit myBytesIo.close() call fixed the leak. Ymmv. Note that I’m on python 3.8.2 on MacOS, not sure it’s the latest…