Skip to content
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

[Cross-Posted] ipyfilechooser + ipython_blocking = large memory leak #36

Closed
bryceschober opened this issue Feb 4, 2021 · 5 comments
Closed

Comments

@bryceschober
Copy link

When I:

  1. Run my example from https://github.com/bryceschober/ipyfilechooser_blocking_example in Jupyter or VS Code or even the binder.org link on that example repository.
  2. Choose to run all cells instead of one at a time.
  3. Observe a large continuous memory leak until you actually select a file and click select, and then memory usage returns to normal.
@bryceschober bryceschober changed the title [Cross-Posted] [Cross-Posted] ipyfilechooser + ipython_blocking = large memory leak Feb 4, 2021
@kafonek
Copy link

kafonek commented Feb 5, 2021

@bryceschober I am able to replicate your behavior in Binder. I don't have any immediate hunch on why that is happening, and debugging memory leaks is well outside my bailiwick. Perhaps someone with more kernel or ipywidgets expertise will pick up something obvious? I looked around for "Python memory leak debug strategies" and came across pympler.

Modifying your cells slightly in the ipython_blocking repo binder, I used --

# cell 1
!pip install ipyfilechooser pympler -q
# cell 2
import ipython_blocking
from pympler import tracker
from ipyfilechooser import FileChooser
from pathlib import Path

# Create and display a FileChooser widget
fc = FileChooser(str(Path.home().resolve()), use_dir_icons=True)

tr = tracker.SummaryTracker()
def path_is_selected():
    tr.print_diff()
    return bool(fc.selected)
display(fc)
# cell 3
%block path_is_selected
# cell 4
print(fc.selected_path)
print(fc.selected_filename)
print(fc.selected)

Between the callback in cell 2 and the %block function in cell 3, it should mean that every time get_ipython().kernel.do_one_iteration() runs, it prints out a diff in memory tracking from before and after that call. I do not really know how to interpret the output of this, but I see blocks like this over and over again,

                  types |   # objects |   total size
======================= | =========== | ============
                   dict |           1 |    160     B
    function (<lambda>) |           1 |    144     B
        _asyncio.Future |           1 |    136     B
              generator |           1 |    128     B
                  tuple |           2 |    128     B
  asyncio.events.Handle |           1 |    112     B
                Context |           1 |     80     B
     tornado.gen.Runner |           1 |     64     B
                   cell |           1 |     56     B
                   list |           0 |    -80     B
                    str |          -4 |   -424     B
                  types |   # objects |   total size
======================= | =========== | ============
                  tuple |           3 |    200     B
                   dict |           1 |    160     B
                    str |           2 |    147     B
    function (<lambda>) |           1 |    144     B
        _asyncio.Future |           1 |    136     B
              generator |           1 |    128     B
  asyncio.events.Handle |           1 |    112     B
                Context |           1 |     80     B
                   list |           0 |     80     B
     tornado.gen.Runner |           1 |     64     B
                   cell |           1 |     56     B
                  float |           1 |     24     B

Perhaps @minrk or @jasongrout see something obvious that jumps out at them?

@crahan
Copy link
Owner

crahan commented Feb 5, 2021

I'm definitely not a Python memory leak debugging expert either (quite the opposite). I'm wondering though if the same type of memory leak occurs when you use %block with a function that waits until an IntSlider (https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#IntSlider) has a specific value. I'm trying to figure out if it's a widget issue or a filechooser issue.

@kafonek
Copy link

kafonek commented Feb 5, 2021

@crahan after a little more poking at this, it looks like the memory leak is happening with pretty much any widget. ipyfilechooser, IntSlider, Checkbox, and Text are the ones I tried. I also tried replicating this using the explicit context manager syntax instead of %block magic and got the same behavior, so I don't think it's a magic thing. I added some print statements into the CaptureExecution.capture_event and CaptureExecution.step methods just to sanity check it wasn't storing anything unexpected or adding variables to global/local scope and that doesn't appear to be the case.

My next step in this debug journey was trying out tracemalloc. I used their recipe for this syntax --

### cell 1
import linecache
import os
import tracemalloc

def display_top(snapshot, key_type='lineno', limit=10):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        print("#%s: %s:%s: %.1f KiB"
              % (index, frame.filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))
    
tracemalloc.start()

### cell 2
import ipywidgets as widgets

slider = widgets.IntSlider(min=0, max=10)
slider


### cell 3
import ipython_blocking

ctx = ipython_blocking.CaptureExecution()
with ctx:
    i = 0
    while True:
        if slider.value >= 5:
            break
        ctx.step()
        i += 1
        if i == 100000:
            snapshot = tracemalloc.take_snapshot()
            break

### cell 4
print(slider.value)

### cell 5
display_top(snapshot, limit=20)

It took mybinder maybe 20 seconds to hit that 100k do_one_iteration() marker, and the memory spiked by a few hundred MB as we expected while it ran. Once it was done it printed out this output:

Top 20 lines
#1: /srv/conda/envs/notebook/lib/python3.7/site-packages/tornado/gen.py:226: 21875.8 KiB
    future.add_done_callback(lambda _: runner)
#2: /srv/conda/envs/notebook/lib/python3.7/site-packages/tornado/gen.py:142: 13282.3 KiB
    future = Future()  # type: Future
#3: /srv/conda/envs/notebook/lib/python3.7/site-packages/tornado/gen.py:191: 12503.8 KiB
    result = func(*args, **kwargs)
#4: /srv/conda/envs/notebook/lib/python3.7/asyncio/base_events.py:711: 10938.9 KiB
    handle = events.Handle(callback, args, self, context)
#5: /srv/conda/envs/notebook/lib/python3.7/site-packages/tornado/gen.py:706: 7812.9 KiB
    self.gen = gen
#6: /srv/conda/envs/notebook/lib/python3.7/site-packages/tornado/gen.py:772: 7810.3 KiB
    self.future = convert_yielded(yielded)
#7: /srv/conda/envs/notebook/lib/python3.7/site-packages/tornado/gen.py:225: 6250.3 KiB
    runner = Runner(result, future, yielded)
#8: /srv/conda/envs/notebook/lib/python3.7/site-packages/zmq/sugar/attrsettr.py:52: 6214.9 KiB
    return self.get(opt)
#9: /srv/conda/envs/notebook/lib/python3.7/site-packages/tornado/gen.py:202: 6214.2 KiB
    if isinstance(result, Generator):
#10: /home/jovyan/ipython_blocking/ipython_blocking.py:21: 5468.8 KiB
    self.kernel.do_one_iteration()
#11: /srv/conda/envs/notebook/lib/python3.7/asyncio/base_events.py:714: 802.8 KiB
    self._ready.append(handle)
#12: <frozen importlib._bootstrap_external>:525: 272.6 KiB
#13: /srv/conda/envs/notebook/lib/python3.7/linecache.py:137: 236.2 KiB
    lines = fp.readlines()
#14: /srv/conda/envs/notebook/lib/python3.7/site-packages/traitlets/traitlets.py:735: 159.0 KiB
    return super(MetaHasDescriptors, mcls).__new__(mcls, name, bases, classdict)
#15: /srv/conda/envs/notebook/lib/python3.7/site-packages/traitlets/traitlets.py:459: 85.2 KiB
    self.metadata = self.metadata.copy()
#16: /srv/conda/envs/notebook/lib/python3.7/tracemalloc.py:185: 69.2 KiB
    self._frames = tuple(reversed(frames))
#17: /srv/conda/envs/notebook/lib/python3.7/site-packages/traitlets/traitlets.py:431: 37.3 KiB
    self.default_value = default_value
#18: /srv/conda/envs/notebook/lib/python3.7/site-packages/traitlets/traitlets.py:394: 18.9 KiB
    self.name = name
#19: /srv/conda/envs/notebook/lib/python3.7/site-packages/ipywidgets/widgets/docutils.py:11: 16.7 KiB
    cls.__doc__ = cls.__doc__.format(**stripped_snippets)
#20: /srv/conda/envs/notebook/lib/python3.7/site-packages/traitlets/traitlets.py:758: 13.5 KiB
    cls._trait_default_generators = {}
1202 other: 569.0 KiB
Total allocated size: 100652.7 KiB

ipykernel's do_one_iteration() does create a tornado generator but I don't know where to go from there.

@crahan
Copy link
Owner

crahan commented Feb 7, 2021

Not sure what the best way is to proceed with this. It does appear from your testing that it's happening across different widgets (including default ipywidgets) and isn't specific to just ipyfilechooser. If anything needs to be modified for ipyfilechooser to help resolve this, I'm happy to investigate further, but I doubt there's much I can do.

@crahan
Copy link
Owner

crahan commented Apr 3, 2021

I'll go ahead and close this issue as it appears that this issue is not specifically related to just ipyfilechooser. If this is in error, please feel free to comment and I can look at investigating further.

@crahan crahan closed this as completed Apr 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants