Skip to content

[Bug]: plt.figure(), plt.close() leaks memory #23701

Closed

Description

@nschloe

Bug summary

MWE:

import matplotlib.pyplot as plt

while True:
    plt.figure()
    plt.close()

Either run this code and monitor the mem consumption on top, or run

mprof run test.py
mprof plot

(with memory-profiler) to get

screenshot

Might be related to #22448.

Operating system

Ubuntu 22.10

Matplotlib Version

3.5.3

Matplotlib Backend

GTK4Agg

Python version

3.10.6

Jupyter version

No response

Installation

pip

Activity

  1. nschloe commented on Aug 22, 2022

    @nschloe
    ContributorAuthor

    The following is with

    matplotlib.use('GTK3Agg')

    screenshot

  2. timhoffm commented on Aug 22, 2022

    @timhoffm
    Member

    Have you tried other backends? Is this GTK and/or AGG specific? Interesting would be "qtagg", "Tkagg", "agg" and "gtk4cairo".

  3. tacaswell commented on Aug 22, 2022

    @tacaswell
    Member

    The issue is that you are never letting the GUI event loop run so there are a growing number of GUI windows just hanging out in the background waiting for the event loop to run so they can finish their tear down cycle (because most UI frameworks have a way to notify that things are being destroyed etc).

    If you need to run tight loops like this either use a non-interactive backend (like 'agg' or 'svg') which will never create the GUI objects or call plt.pause(.1) every so often to let the GUI clean up.

  4. nschloe commented on Aug 22, 2022

    @nschloe
    ContributorAuthor

    I've tried the suggestion. Outcome: All backends have the problem, but 'agg' and 'svg' run a lot faster than the rest. I've tried

    • 'agg'
    • 'gtk3agg'
    • 'gtk3cairo'
    • 'gtk4agg'
    • 'gtk4cairo'
    • 'qtagg'
    • 'qtcairo'
    • 'svg'

    When adding plt.pause(1.0) (a full second), no clean-up appears to happen. This is with 'agg'; observe the plateaus:

    agg-pause-1-0

    Code:

    import matplotlib
    import matplotlib.pyplot as plt
    
    matplotlib.use("GTK4Agg")
    
    # [
    #     "GTK3Agg",
    #     "GTK3Cairo",
    #     "GTK4Agg",
    #     "GTK4Cairo",
    #     "MacOSX",
    #     "nbAgg",
    #     "QtAgg",
    #     "QtCairo",
    #     "Qt5Agg",
    #     "Qt5Cairo",
    #     "TkAgg",
    #     "TkCairo",
    #     "WebAgg",
    #     "WX",
    #     "WXAgg",
    #     "WXCairo",
    #     "agg",
    #     "cairo",
    #     "pdf",
    #     "pgf",
    #     "ps",
    #     "svg",
    #     "template",
    # ]
    
    for k in range(4000):  # higher numbers for agg, svg
        plt.figure()
        plt.close()
        if k % 1000 == 0:
            plt.pause(1.0)
  5. tacaswell commented on Aug 22, 2022

    @tacaswell
    Member

    Can you please try adding gc.collect() (to clean up an reference cycles) and put the pause before the close (some of the backends do not actually run the event loop if the all of the managers are gone).

    Can you also try with https://github.com/matplotlib/matplotlib/blob/main/tools/memleak.py on your system?

    import gc
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    mpl.use('agg')
    
    while True:
        plt.figure()
        plt.close()
        gc.collect()

    This stays under 90MB for me (where as with out the gc.collect() it did indeed start to run away).

  6. tacaswell commented on Aug 22, 2022

    @tacaswell
    Member
    import matplotlib.figure as mfigure
    
    
    while True:
        mfigure.Figure()

    Also seems to work so likely something in the Manager class

  7. nschloe commented on Aug 22, 2022

    @nschloe
    ContributorAuthor
    • agg without gc (100k iterations):

      agg

    • agg with gc (100k iterations, gc after every 1000 its):

      agg-gc

  8. nschloe commented on Aug 22, 2022

    @nschloe
    ContributorAuthor
    python3 memleak.py agg 100 out.pdf
    

    screenshot

  9. nschloe commented on Aug 22, 2022

    @nschloe
    ContributorAuthor

    mfigure Also seems to work so likely something in the Manager class

    Indeed:

    import gc
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    import matplotlib.figure as mfigure
    
    mpl.use('agg')
    
    for k in range(100000):
        mfigure.Figure()
        plt.close()

    gives

    screenshot

    -- no explicit gc.

  10. tacaswell commented on Aug 23, 2022

    @tacaswell
    Member

    That long delay at the end is probably GC finally running on the way out

    plt.close() is no effect in that second example because the Figure was never registered with


    I understand the problem, TL;DR either use manual gc.collect() or set gc.set_threshold(100) because we internally use gc.collect(1) which breaks CPyhons garbage collection (I'll be opening up a PR soon).

    1. https://devguide.python.org/internals/garbage-collector/index.html is the best write of the GC out there
    2. a details that is not included is that the logic to sort out which generation to run is only run if the threshold for the lowest generation is met (cpython source)
    3. we have a gc.collect(1) in
      # Full cyclic garbage collection may be too expensive to do on every
      # figure destruction, so we collect only the youngest two generations.
      # see: https://github.com/matplotlib/matplotlib/pull/3045
      gc.collect(1)
      which is sweeping out lower generations
    4. if we do not have a code path between the plt.close() that does not make 700 objects then we never pass that first test and CPython never tries to run its "what generation to run?" logic (cpython source) and we have this run-away "leak"

    You can reproduce this issue with:

    import gc
    import matplotlib.figure as mfigure
    
    
    print("about to start")
    for j in range(100_000):
        fig = mfigure.Figure()
        gc.collect(1)
        print(gc.get_count())

    A way to "fix" this is

    from itertools import count
    import gc
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    mpl.use('agg')
    
    
    class MyObject:
        ...
        
    gc.collect()
    print("about to start")
    for j in range(100):
        print(gc.get_count())
        plt.figure()
        print(gc.get_count())
        plt.close()
        print(gc.get_count())
        c = [MyObject() for j in range(700)]

    which will force enough generation 0 objects to be created that it will cause a full gc eventually.

  11. added this to the v3.5.4 milestone on Aug 23, 2022
  12. added a commit that references this issue on Aug 23, 2022
    9f3861f
  13. modified the milestones: v3.5.4, v3.6.0 on Aug 25, 2022
  14. added a commit that references this issue on Dec 19, 2022
    03ff3b0
  15. added 2 commits that reference this issue on Apr 19, 2026
    db8ad3a
    bf50e9f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions