Skip to content

Unnecessary drawing with NbAgg #13971

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
astrofrog opened this issue Apr 16, 2019 · 20 comments · Fixed by #17348
Closed

Unnecessary drawing with NbAgg #13971

astrofrog opened this issue Apr 16, 2019 · 20 comments · Fixed by #17348
Assignees
Milestone

Comments

@astrofrog
Copy link
Contributor

I am running into performance issues when using the NbAgg backend, some of which I think is avoidable. Essentially, when clicking on an NbAgg plot, with no tool active, the backend re-draws the data, which can cause the kernel to hang if the plot is complex. Here is a demonstration - note the kernel activity in the circle in the top right. Every time I click on the plot, it is redrawn:

Screen-Recording-2019-04-16-at-11 13 08

Just to be clear, I totally expect plotting to be slow the first time the plot is shown, and if e.g. panning/zooming, but drawing when no tool is active doesn't seem right. This is causing issues for me because I am developing tools that respond to a click event in the plot and try and be nice by doing draw_idle to keep the UI responsive, but every mouse action also results in a full draw which slows things down a lot.

Matplotlib version

  • Operating system: MacOS X 10.14.2
  • Matplotlib version: latest dev (557cdfa)
  • Matplotlib backend (print(matplotlib.get_backend())): NbAgg
  • Python version: 3.7.1
  • Jupyter version (if applicable): 4.4.0
@astrofrog astrofrog changed the title Unnecessary drawing with nbagg Unnecessary drawing with NbAgg Apr 16, 2019
@thomasaarholt
Copy link

thomasaarholt commented Jun 22, 2019

I just did some testing:
MWE:

import numpy as np
import matplotlib.pyplot as plt

i = 1000000
y = np.random.random(i)
x = np.random.random(i)
plt.figure()
plt.scatter(x,y)
  • This behaviour is also present with the widget backend
  • It is not present in the qt5 backend

@SylvainCorlay I see you're doing a lot of updates to ipympl these days (super job, btw). Have you seen anything that might affect this?

@timhoffm
Copy link
Member

Given #14596

Going forward we should [...] deprecate the nbagg backend

is it still worth looking into performance issues of nbagg?

@SylvainCorlay
Copy link
Member

@astrofrog it would be interesting if you could try out the master version of ipympl and let us know if this improves your experience. We are funding @martinRenou to put some cycles into ipympl, which now makes use of ipywidgets' binary messages, so that no base64 encoding is required. This should make things a bit smother hopefully.

@SylvainCorlay
Copy link
Member

@SylvainCorlay I see you're doing a lot of updates to ipympl these days (super job, btw). Have you seen anything that might affect this?

Yes, I don't think the current logic in ipympl / nbagg is right. At the moment, @martinRenou is mostly doing a cleanup work / removing dead code / removing dependency to jquery. Goals of the pass by martin are:

  • a general cleanup and modernization of the code
  • making ipympl use more of the underlying ipywidgets tools rather than redoing so much manually

Hopefully, a lot of the current glitches should be solved with this pass, and then the code base should be easier to move forward.

Another goal is to have the ipympl front-end to be a bit more stateful, which would help for some of the embedding contexts.

Going forward we should [...] deprecate the nbagg backend

Yes, I think so too.

@martinRenou
Copy link
Member

Concerning this "redraw on mouse move" issue, from what I understand I feel like matplotlib itself is requesting a redraw, so either we are sure that nothing changed and we don't redraw or we always redraw on request...

The thing is that sometimes it really needs a redraw on mouse move (e.g. displaying a label on mouse hover).

@tacaswell
Copy link
Member

how are we shipping the cursor location text to the front end? I wonder if that is accidentally causing the re-draw?

@tacaswell tacaswell added this to the v3.2.0 milestone Jun 24, 2019
@martinRenou
Copy link
Member

martinRenou commented Jun 25, 2019

how are we shipping the cursor location text to the front end?

If I understand correctly, the cursor location text is sent from here:

def send_message(self, event):

And this code is triggered when the front-end sends a motion_notify event.

Actually, I tried with ipympl and moving the mouse around does not seem to trigger a redraw. As ipympl does not change that much code on the back-end compared to NbAgg, I would be tempted to say that it's due to the NbAgg front-end implementation.

@tacaswell tacaswell modified the milestones: v3.2.0, needs sorting Sep 10, 2019
@QuLogic
Copy link
Member

QuLogic commented Apr 4, 2020

I don't see any indication that there's any drawing happening in your screencast, other than the "busy" indicator.

I ran latest master and looked at Dev Tools, and the websocket only has a mouse_move event and a corresponding status message update. This is translating your mouse position into the "x=0.12345 y=6.7890" message in the toolbar.

There are also some messages flipping the execution status from idle to busy and back. I think that Jupyter is just flipping execution status any time it has to run Python code that's not itself. Maybe with the binary protocol, this can use a different channel, or be flagged to not do that.

@QuLogic
Copy link
Member

QuLogic commented Apr 4, 2020

Sorry, for some reason, I totally spaced out on the clicking part of the reproducer, which I can confirm. And I can reproduce that on latest master, though not yet sure what is triggering it.

@QuLogic
Copy link
Member

QuLogic commented Apr 4, 2020

So the draw is only an empty blit because nothing's changed, but since transferring as base64 is not too efficient, it's a bit slow, though not as much as a full draw. Probably ipympl using binary transfer means this is less perceptible, but it may still be getting the extra draw.

After a while of digging, the cause of the draw seems to come from the IPython post execute hook:

def post_execute():
if matplotlib.is_interactive():
draw_all()

I don't think that hook is necessary in the notebook, so you could deregister it by calling plt.uninstall_repl_displayhook.

While that may be a suitable workaround for you, it's not the real cause because draw_all is supposed to only redraw figures that are stale:

for manager in cls.get_all_fig_managers():
if force or manager.canvas.figure.stale:
manager.canvas.draw_idle()

@QuLogic
Copy link
Member

QuLogic commented Apr 4, 2020

Annnnd the reason it's stale is in a round about way related to #6664:

  File "matplotlib/lib/matplotlib/backend_bases.py", line 1754, in button_press_event
    self.callbacks.process(s, mouseevent)
  File "matplotlib/lib/matplotlib/cbook/__init__.py", line 225, in process
    func(*args, **kwargs)
  File "matplotlib/lib/matplotlib/backend_bases.py", line 1647, in pick
    self.figure.pick(mouseevent)
  File "matplotlib/lib/matplotlib/artist.py", line 513, in pick
    a.pick(mouseevent)
  File "matplotlib/lib/matplotlib/artist.py", line 513, in pick
    a.pick(mouseevent)
  File "matplotlib/lib/matplotlib/artist.py", line 502, in pick
    for a in self.get_children():
  File "matplotlib/lib/matplotlib/axis.py", line 751, in get_children
    *self.get_major_ticks(), *self.get_minor_ticks()]
  File "matplotlib/lib/matplotlib/axis.py", line 1325, in get_major_ticks
    numticks = len(self.get_minorticklocs())
  File "matplotlib/lib/matplotlib/axis.py", line 1243, in get_majorticklocs
    major_locs = self.major.locator()
  File "matplotlib/lib/matplotlib/ticker.py", line 2123, in __call__
    return self.tick_values(vmin, vmax)
  File "matplotlib/lib/matplotlib/ticker.py", line 2131, in tick_values
    locs = self._raw_ticks(vmin, vmax)
  File "matplotlib/lib/matplotlib/ticker.py", line 2070, in _raw_ticks
    nbins = np.clip(self.axis.get_tick_space(),
  File "matplotlib/lib/matplotlib/axis.py", line 2092, in get_tick_space
    tick = self._get_tick(True)
  File "matplotlib/lib/matplotlib/axis.py", line 1860, in _get_tick
    return XTick(self.axes, 0, major=major, **tick_kw)
  File "matplotlib/lib/matplotlib/axis.py", line 401, in __init__
    super().__init__(*args, **kwargs)
  File "matplotlib/lib/matplotlib/cbook/deprecation.py", line 387, in wrapper
    return func(*inner_args, **inner_kwargs)
  File "matplotlib/lib/matplotlib/axis.py", line 146, in __init__
    self.apply_tickdir(tickdir)
  File "matplotlib/lib/matplotlib/axis.py", line 449, in apply_tickdir
    self.stale = True
  File "matplotlib/lib/matplotlib/artist.py", line 226, in stale
    self.stale_callback(self, val)
  File "matplotlib/lib/matplotlib/artist.py", line 54, in _stale_axes_callback
    self.axes.stale = val
  File "matplotlib/lib/matplotlib/artist.py", line 226, in stale
    self.stale_callback(self, val)
  File "matplotlib/lib/matplotlib/figure.py", line 43, in _stale_figure_callback
    self.figure.stale = val
  File "matplotlib/lib/matplotlib/figure.py", line 246, in stale

Button press starts picking -> Figure.pick -> Axes.pick -> Axis.pick -> Axis.get_children -> Axis.get_major_ticks -> stuff -> new XTick, which updates its position and this marks the Axis as stale (propagating up to the Figure.)

@tacaswell tacaswell modified the milestones: needs sorting, v3.3.0 Apr 4, 2020
@tacaswell
Copy link
Member

Do we need a more general way to suppress the stale processing or is the problem that we are getting a new xtick?

@anntzer
Copy link
Contributor

anntzer commented Apr 4, 2020

I guess #11027 is related.

@QuLogic QuLogic self-assigned this Apr 4, 2020
@QuLogic
Copy link
Member

QuLogic commented Apr 7, 2020

I don't think #11027 would have any effect on this. Current options seem to be:

  • Don't use Axis._get_tick, but just find the font size from the stored kwargs and default rcParams. This mostly works, though it is slightly problematic for Axis subclasses (such as polar ones), because they don't have their own rcParams. See QuLogic@afbb048
  • Override stale processing when calling Axis._get_tick. See QuLogic@9411bbd

Both of these fix the extra stale-ness, so if there's any preference, I can open a PR from either branch. I'm somewhat partial to the first method, but the possible effect on Axis subclasses is a bit of an unknown.

One approach I haven't tried is to check Axis.majorTicks for a Tick first.

@tacaswell
Copy link
Member

I'm also partial to the first as it is simpler at run time.

@anntzer
Copy link
Contributor

anntzer commented Apr 7, 2020

First option would also "fail" if the user manually changed the fontsize of the tick and expects that to be taken into account in ticknumber selection. Carefully checking Axis.majorTicks for an already existing tick (without triggering unlazification!), i.e. the last suggestion, may work, though.

The link to #11027 was more "if we're going to change things here, maybe we want to design something that'll sit better with #11027."

@QuLogic
Copy link
Member

QuLogic commented Apr 7, 2020

If they changed the fontsize of a single tick, then that wouldn't have been taken into account in the current code either? But anyway, the Tick properties are not canonical (which is probably annoying, but a different issue), the kwargs dictionary on the Axis are.

And yes, the unlazification is the tricky part.

@QuLogic
Copy link
Member

QuLogic commented Apr 30, 2020

I've put together the third option here: QuLogic@d0b2334

Opinions on which one? I think I still prefer the first one.

@tacaswell
Copy link
Member

I think I like the first one best. The second and third feel like they are spending too much effort to work around our selves which is sign we should reconsider what we are doing.

It is a bit annoying that the Tick object attaching itself to the Axes enough to participate in the stale tree even though it will never be shown on screen, however fixing that looks like a much bigger effort...

@QuLogic
Copy link
Member

QuLogic commented May 7, 2020

Took me a little bit to figure out the test, as this is only triggered with mpl20 style, not classic, but I've opened #17348 with the first option.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants