10000 Memory leaks on matplotlib 3.4.2 (and 3.4.0) · Issue #20490 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Memory leaks on matplotlib 3.4.2 (and 3.4.0) #20490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
fohrloop opened this issue Jun 23, 2021 · 10 comments · Fixed by #22002
Closed

Memory leaks on matplotlib 3.4.2 (and 3.4.0) #20490

fohrloop opened this issue Jun 23, 2021 · 10 comments · Fixed by #22002
Assignees
Labels
Milestone

Comments

@fohrloop
Copy link

Bug report

Bug summary

Multiple different memory related errors when running as a part of script creating and saving lots of figures.

Code for reproduction

Sorry, could not create a minimal example that reproduces this. Just wanted to leave a note.

Actual outcome

There are multiple different errors that happen. Running the same script will produce different results randomly.

Example 1
Just prints this. No Exceptions, nothing. Script stops running.

unable to alloc 4320000 bytes

Example 2

  File "c:\myapp\calculations\myscript.py", line 422, in create_figure
    fig, ax = plt.subplots(nrows=1, figsize=(16, 9))
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\_api\deprecation.py", line 471, in wrapper
    return func(*args, **kwargs)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\pyplot.py", line 1439, in subplots
    fig = figure(**fig_kw)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\pyplot.py", line 797, in figure
    manager = new_figure_manager(
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\pyplot.py", line 316, in new_figure_manager
    return _backend_mod.new_figure_manager(*args, **kwargs)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\backend_bases.py", line 3545, in new_figure_manager
    return cls.new_figure_manager_given_figure(num, fig)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\backends\_backend_tk.py", line 899, in new_figure_manager_given_figure
    canvas = cls.FigureCanvas(figure, master=window)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\_api\deprecation.py", line 431, in wrapper
    return func(*inner_args, **inner_kwargs)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\backends\_backend_tk.py", line 174, in __init__
    self._tkphoto = tk.PhotoImage(
  File "C:\Python\Python 3.8.6-32\lib\tkinter\__init__.py", line 4061, in __init__
    Image.__init__(self, 'photo', name, cnf, master, **kw)
  File "C:\Python\Python 3.8.6-32\lib\tkinter\__init__.py", line 4006, in __init__
    self.tk.call(('image', 'create', imgtype, name,) + options)
_tkinter.TclError: not enough free memory for image buffer

Example 3

  File "c:\myapp\calculations\myscript.py", line 467, in create_some_figures
    plt.savefig(self.folder / "myfig.png")
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\pyplot.py", line 966, in savefig
    res = fig.savefig(*args, **kwargs)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\figure.py", line 3005, in savefig
    self.canvas.print_figure(fname, **kwargs)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\backend_bases.py", line 2255, in print_figure
    result = print_method(
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\backend_bases.py", line 1669, in wrapper
    return func(*args, **kwargs)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\backends\backend_agg.py", line 508, in print_png
    FigureCanvasAgg.draw(self)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\backends\backend_agg.py", line 401, in draw
    self.renderer = self.get_renderer(cleared=True)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\backends\backend_agg.py", line 417, in get_renderer
    self.renderer = RendererAgg(w, h, self.figure.dpi)
  File "C:\Python\venvs\adiapp\lib\site-packages\matplotlib\backends\backend_agg.py", line 91, in __init__
    self._renderer = _RendererAgg(int(width), int(height), dpi)
MemoryError: In RendererAgg: Out of memory

Expected outcome

No errors.

Matplotlib version

  • Operating system: Windows 10
  • Matplotlib version (import matplotlib; print(matplotlib.__version__)): 3.4.2
  • Matplotlib backend (pri 8000 nt(matplotlib.get_backend())): TkAgg
  • Python version: 32-bit 3.8.6

Workaround

As a side note, tried on version 3.3.4 and everything works. Does not work on 3.4.0. I'm calling

        plt.close("all")
        plt.close()
        gc.collect()

after every plt.savefig() just to be sure.

@tacaswell
Copy link
Member

If you put

import matplotlib
matplotlib.use('agg')

at the top of your script do these issues go away?

I suspect that adding plt.pause(.01) would also "solve" the problem.

When you create a figure using a GUI backend, we are also creating GUI objects behind the scenes. These objects can out-live the Figures and may rely on the GUI event loop to be run in order to (safely) tear them selves down and return the (c/c++ side) memory to the system. Previously we were doing some not-recommended things in Tk may have been allowing the objects to be torn down, but for 3.4 we brought our Tk event loop management in-line with best practices which may have caused this issue..

attn @richardsheridan

@richardsheridan
Copy link
Contributor

Agree, linking relevant PRs #9956, #17789, #19959. Also curious if other backends leak like this, for instance Qt or wx.

In this comment I said that update (or flush_events or pause) would need to be called in obscure corner cases, but a plot/savefig loop could hardly be considered obscure, so we may have to revisit this if it indeed leads to a memory leak. OTOH plot/show(block=False)/savefig might be considered behavior that we don't need to facilitate.

@richardsheridan richardsheridan self-assigned this Aug 9, 2021
@richardsheridan
Copy link
Contributor

Tinkering on master to identify the leak, here is an interesting MRE:

import psutil, objgraph

import matplotlib
matplotlib.use('tkagg')  # leaks
# matplotlib.use('qt5agg')  # leaks (pyside2)
# matplotlib.use('agg')  # no leaks
import matplotlib.pyplot as plt

proc = psutil.Process()

for i in range(50):
    print(i, proc.memory_info().rss)
    objgraph.show_growth()
    plt.plot([1,2,5])
    plt.savefig(f"fig{i}.png")
    # plt.pause(.1)  # doesn't patch the leak, but causes QT to crash
    plt.clf()  # or cla() to simulate use case of plotting fresh figures
    plt.close()  # comment to patch the leak
    # plt.pause(.1)  # only calls time.sleep(.1) here

Specifically regarding the tk backend, window.update_idletasks vs delayed_destroy makes no difference to the leakiness in my testing.

@mbway
Copy link
mbway commented Dec 17, 2021

I have also encountered this issue. I have an example that is even more minimal. No plotting or saving is necessary to leak memory.
The example may not seem like a problem because you are unlikely to be plotting 100,000 images, but
when using ax.imshow() even just 100 images can consume a lot of memory.

In a fresh virtual environment with python 3.9.9 and only matplotlib the following code leaks:

import matplotlib
from matplotlib import pyplot as plt

matplotlib.use('tkagg')
for i in range(100_000):
    fig = plt.figure()
    plt.close(fig)

any of the following workarounds make no difference:

fig.clf()
plt.close('all')
del fig
gc.collect()

however using qt5agg or just agg does not leak any memory.

I have tested matplotlib versions 3.5.1, 3.5.0, 3.4.0, 3.4.0rc1 and 3.3.4.
The issue was introduced between 3.3.4 and 3.4.0rc1 and behaves the same for every version after that.
I would have bisected to narrow down the commits further but I am unable to build matplotlib on my machine
(I get ImportError: ft2font.cpython-39-x86_64-linux-gnu.so: undefined symbol: FT_Load_Glyph when built from source)

@tacaswell
Copy link
Member

The core of the problem is that if you are using a GUI backend we are creating c++ side objects for the GUI framework. Those have their own life cycle rules and if you never run the GUI main loop in some cases they can not clean them selves up.

ImportError: ft2font.cpython-39-x86_64-linux-gnu.so: undefined symbol: FT_Load_Glyph

I suggest

git clean -xfd
pip install -ve .

@mbway
Copy link
mbway commented Dec 18, 2021

I see, so technically the memory is not leaked (references still exist) but there is no way to directly free it.

If I need to do a lot of plotting it seems like the only option is to detect if tkagg is active and switch to something else instead since I can manually switch back in the rare case when I need plt.show. That isn't ideal but should be OK for my use case.

I'm trying to track down some memory leaks which are likely a result of some exotic uses of the library and this was the only leak I could pin down definitively as not being my fault.
I have some code which pickles figures and sends them to subprocesses via a queue for rasterisation and saving.
Normally this approach works very well but there are very specific cases (which I can't quite pin down) where huge amounts of memory are leaked but it's very hard to diagnose by looking only at the python code when stuff is allocated behind the scenes and then not freed.

@richardsheridan
Copy link
Contributor
richardsheridan commented Dec 18, 2021

The example by @mbway can be improved with psutil, showing a growth of around 5 MB per loop.

import matplotlib
from matplotlib import pyplot as plt
import psutil

p = psutil.Process()
matplotlib.use('tkagg')
for i in range(100_000):
    fig = plt.figure()
    plt.close(fig)
    print(p.memory_full_info().uss // 1_000_000)

This figure-based leak bisects to e201189, which is no great surprise. I am working on a patch for that. What is surprising is that the leak persists (although at a slower rate of growth) on main even when you revert that commit!

I bisected again with that reversion applied and got back 741ee02, which is part of #19167 (?!) so @QuLogic might have to dig in to find something leaky in there... at a glance I can't pick anything suspicious out.

@QuLogic
Copy link
Member
QuLogic commented Apr 6, 2022

I bisected again with that reversion applied and got back 741ee02, which is part of #19167 (?!) so @QuLogic might have to dig in to find something leaky in there... at a glance I can't pick anything suspicious out.

I cannot reproduce this; if I revert that commit, then the memory leak amount from #22002 remains the same.

@richardsheridan
Copy link
Contributor

Something may have happened on main or a dependency or my test setup since I wrote that comment and the PR. Either way, at the moment on the rebased the PR branch, memory profiling shows very little sensitivity to my original fix and I also don't see any issues with the hidpi stuff.

It does suggest to me that we should put in at least the memory leak check (and possibly others) to prevent more regressions.

@QuLogic
Copy link
Member
QuLogic commented Apr 28, 2022

With #22002 as it is now, the above test is down to about 1-2MB, but once you insert a gc.collect in the loop, then there is no leak after 10 iterations.

tacaswell pushed a commit that referenced this issue Apr 28, 2022
tkinter variables get cleaned up with normal `destroy` and `gc` semantics but tkinter's implementation of trace is effectively global and keeps the callback object alive until the trace is removed.

Additionally extend and clean up the tests.

Closes #20490

Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
tacaswell pushed a commit to tacaswell/matplotlib that referenced this issue Apr 29, 2022
…ory growth regressions

FIX: TkAgg memory leaks and test for memory growth regressions (matplotlib#22002)

tkinter variables get cleaned up with normal `destroy` and `gc` semantics but tkinter's implementation of trace is effectively global and keeps the callback object alive until the trace is removed.

Additionally extend and clean up the tests.

Closes matplotlib#20490

Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
(cherry picked from commit 1a016f0)
tacaswell pushed a commit to tacaswell/matplotlib that referenced this issue Apr 29, 2022
…ory growth regressions

FIX: TkAgg memory leaks and test for memory growth regressions (matplotlib#22002)

tkinter variables get cleaned up with normal `destroy` and `gc` semantics but tkinter's implementation of trace is effectively global and keeps the callback object alive until the trace is removed.

Additionally extend and clean up the tests.

Closes matplotlib#20490

Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
(cherry picked from commit 1a016f0)
@QuLogic QuLogic added this to the v3.5.2 milestone Apr 29, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants
0