Skip to content

2.1.0 serious regression in Qt5 backend #9406

Closed

Description

@kawing-chiu

Bug report

Bug summary

My app worked well in previous matplotlib versions. After upgrading to 2.1.0, it is completely broken, flooded with QWidget::repaint: Recursive repaint detected messages.

Code for reproduction

import matplotlib as mpl
import matplotlib.lines
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg

class Qt5Canvas(FigureCanvasQTAgg):
    def __init__(self, parent=None, aspect_ratio=3/4, dpi=96,
            width=None, height=None):
        if width is not None:
            figsize = (width, height)
        else:
            figsize = mpl.figure.figaspect(aspect_ratio)
        fig = mpl.figure.Figure(figsize=figsize, dpi=dpi)
        super().__init__(fig)
        self.fig = fig
        self.ax = fig.add_subplot(111)
        self.setParent(parent)

class TestMplBug:
    def __init__(self):
        self.artist = None
        self.canvas = None
        self._blit_region = None
        self._drawed = False

    def set_canvas(self, canvas):
        self.canvas = canvas
        self.canvas.mpl_connect('draw_event', self._on_mpl_draw)

    def _on_mpl_draw(self, event):
        self._drawed = True
        self.blit_update()

    def draw(self):
        self._on_draw()
        self.canvas.draw()

    def _on_draw(self):
        if self.artist is None:
            import numpy as np
            x = np.arange(0.0, 3.0, 0.01)
            y = np.sin(2*np.pi*x)
            line = mpl.lines.Line2D(x, y, color='red', animated=True)
            self.canvas.ax.add_line(line)
            self.artist = line

    def blit_update(self):
        if self.canvas is None:
            return
        self.canvas.ax.draw_artist(self.artist)
        if self._drawed:
            self.canvas.blit()

def main():
    from PyQt5 import QtWidgets, QtCore, QtGui
    import sys
    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    t = TestMplBug()
    q = Qt5Canvas()
    t.set_canvas(q)
    t.draw()
    t.canvas.show()
    try:
        __IPYTHON__
    except NameError as e:
        app.exec_()
    return t

if __name__ == '__main__':
    main()

Actual outcome

QWidget::repaint: Recursive repaint detected
<many similar entries omited...>
QWidget::repaint: Recursive repaint detected
Traceback (most recent call last):
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/backends/backend_qt5agg.py", line 149, in __draw_idle_agg
    self.draw()
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/backends/backend_qt5agg.py", line 127, in draw
    super(FigureCanvasQTAggBase, self).draw()
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/backends/backend_agg.py", line 430, in draw
    self.figure.draw(self.renderer)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/artist.py", line 55, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/figure.py", line 1295, in draw
    renderer, self, artists, self.suppressComposite)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/image.py", line 138, in _draw_list_compositing_images
    a.draw(renderer)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/artist.py", line 55, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/axes/_base.py", line 2399, in draw
    mimage._draw_list_compositing_images(renderer, self, artists)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/image.py", line 138, in _draw_list_compositing_images
    a.draw(renderer)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/artist.py", line 55, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/axis.py", line 1133, in draw
    ticks_to_draw = self._update_ticks(renderer)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/axis.py", line 974, in _update_ticks
    tick_tups = list(self.iter_ticks())
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/axis.py", line 917, in iter_ticks
    majorLocs = self.major.locator()
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/ticker.py", line 1957, in __call__
    return self.tick_values(vmin, vmax)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/ticker.py", line 1965, in tick_values
    locs = self._raw_ticks(vmin, vmax)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/ticker.py", line 1907, in _raw_ticks
    nbins = np.clip(self.axis.get_tick_space(),
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/axis.py", line 2039, in get_tick_space
    tick = self._get_tick(True)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/axis.py", line 1748, in _get_tick
    return XTick(self.axes, 0, '', major=major, **tick_kw)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/axis.py", line 152, in __init__
    self.apply_tickdir(tickdir)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/axis.py", line 403, in apply_tickdir
    self.stale = True
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/artist.py", line 241, in stale
    self.stale_callback(self, val)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/artist.py", line 68, in _stale_axes_callback
    self.axes.stale = val
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/artist.py", line 241, in stale
    self.stale_callback(self, val)
  File "/home/statistician/.local/lib/python3.6/site-packages/matplotlib/figure.py", line 56, in _stale_figure_callback
    self.figure.stale = val
RecursionError: maximum recursion depth exceeded while calling a Python object
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::end: Painter not active, aborted
<many similar entries omited...>
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::end: Painter not active, aborted

Expected outcome

Draw properly as in previous versions.

Matplotlib version

  • Operating system: Arch Linux
  • Matplotlib version: 2.1.0 (installed from pip)
  • Matplotlib backend (print(matplotlib.get_backend())): qt5
  • Python version: 3.6.2
  • Jupyter version (if applicable):
  • Other libraries:

Activity

  1. tacaswell commented on Oct 15, 2017

    @tacaswell
    Member

    If you comment out

           # if self._drawed:
           #     self.canvas.blit()

    it will work as expected on 2.1 and seems to work correctly on 2.0.2 and 1.5.3.

    The issue is that we now defer actually rendering the canvas as long as physically possible which means that the draw now happens inside of paintEvent. This means that the draw_event and the associated callbacks also happen inside of the paintEvent. You are calling blit which in internally uses repaint which at http://doc.qt.io/qt-5/qwidget.html#repaint comes with the warning

    Warning: If you call repaint() in a function which may itself be called from paintEvent(), you may get infinite recursion. The update() function never causes recursion.

    which is what you are getting here.

    The reason that this works without calling blit is that the figure is first drawn to an internal buffer. Calling draw_artist will update that buffer (but not push it to the screen). In all cases, the draw_event comes out before the buffer is copied to the screen so the blit is redundant, you update the relevant rectangle and then the regular pass through will replace it (and the rest of the canvas).

  2. kawing-chiu commented on Oct 15, 2017

    @kawing-chiu
    Author

    Oh, I see. After using blit() for quite a long time and built complex app on it, I thought I've understood it quite well... So to sum up: 1) After all callbacks for draw_event are processed, matplotlib will handle the copying of the internal buffer into the backend buffer. 2) Calling blit() is not allowed in draw_event's handlers. Should I expect these behaviors in other backends?

  3. tacaswell commented on Oct 15, 2017

    @tacaswell
    Member
    1. Yes, it looks like the only place that CanvasBase.draw_event (which is the function that triggers the draw_event callbacks) is only called at the bottom of Figure.draw so for all of the backends (the ship) the callbacks will be processed before the result of the rendering is pushed to the screen (or saved)

    2. this may be a Qt only restriction and will depend on the details of how the backend implements blitting (or not)

    If it makes you feel any better, I only have this on the top of my head because there was a fair amount of work done around managing dpi changes (from moving Qt windows between screens), resizing the window, and blitting for 2.1.

  4. added this to the v2.1.1 milestone on Oct 15, 2017
  5. added 3 commits that reference this issue on Oct 21, 2017
    8d6e1d3
    5a6ae81
    9b8a944
  6. tacaswell commented on Oct 23, 2017

    @tacaswell
    Member

    Closed by #9505

  7. kawing-chiu commented on Oct 25, 2017

    @kawing-chiu
    Author

    Thank you @tacaswell for the prompt and kind help!

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions