Skip to content

Work around infinite recursion in ToolPalette.resizeEvent() #76

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

Merged
merged 2 commits into from
Feb 8, 2021

Conversation

zakv
Copy link
Contributor

@zakv zakv commented Feb 8, 2021

This PR resolves #27. It takes the approach suggested there of effectively turning off the resize event signal while handling a resize event so as to avoid infinite recursion.

The implementation is a bit different than might be expected. The recursion happens because ToolPalette.resizeEvent() calls ToolPalette._layout_widgets(), which can cause more resize events to be added to the Qt event queue, which in turn causes more calls to resizeEvent(). I'm not aware of any good way to prevent those events from being generated (though I'm not a Qt expert so that could very well be possible). Also, the calls to resizeEvent() execute to completion before it is called again by the event loop. That means that temporarily changing things during the method's execution (such as incrementing/decrementing a recursion level counter or temporarily redefining self.resizeEvent() to avoid recursion) don't work.

The approach taken here is that the first call to resizeEvent() sets self._layout_widgets_during_resizeEvent to False to signal that the upcoming calls of resizeEvent() shouldn't call _layout_widgets(). It then calls _layout_widgets() which may cause resize events to be added to the events queue. Finally before returning, resizeEvent() puts a custom event on the end of the event queue which will cause the ToolPalette to set self._layout_widgets_during_resizeEvent back to True again. That ensures that eventually resize events will cause _layout_widgets() again, which is required for resizing to work well. That custom event isn't handled until all of the resize events generated before it in _layout_widgets() run though, which circumvents the infinite recursion.

I've tested this PR with two problematic blacs settings files (including the one mentioned here) and both crashed without the changes here but started up fine with the changes. I've also seen that once blacs is started, the ToolPalettes still seem to resize just fine; the number of widgets per row still increases/decreases as needed when the tab is expanded/contracted.

…vent() could trigger itself in an infinite resursion loop.
@zakv
Copy link
Contributor Author

zakv commented Feb 8, 2021

There are still a decent number of commented-out python-2-style debugging print statements in toolpalette.py which seem to be oriented towards debugging this issue. I could add a commit to this PR to remove them if desired.

@chrisjbillington
Copy link
Member

Thanks so much for looking into this! Glad to hear the recursion can be prevented.

A tricky solution, I wonder if something simpler would work. Does a simple counter to prevent recursion work, i.e. the following (untested)? If so, that might be preferable.

Feel absolutely free to delete dead code adjacent to the edits you're actually making, if you want to clean-up code elsewhere that's also fine, but generally better as a separate commit!

--- a/labscript_utils/qtwidgets/toolpalette.py
+++ b/labscript_utils/qtwidgets/toolpalette.py
@@ -264,6 +264,8 @@ class ToolPalette(QScrollArea):
         # allocated rows/columns with columnCount()/RowCount() rather than the number of visible ones
         self._column_count = 0
         self._row_count = 0
+        self._resize_recursion_depth = 0
+        self.MAX_RESIZE_RECURSION = 5

     def addWidget(self,widget,force_relayout=True):
         # Append to end of tool pallete
@@ -386,24 +388,25 @@ class ToolPalette(QScrollArea):
         return QSize(width, height)

     def resizeEvent(self, event):
-        # overwrite the resize event!
-        # print '--------- %s'%self._name
-        # print self._widget_list[0].size()
-        # print self._widget_list[0].sizeHint()
-        # print self._widget_list[0].minimumSizeHint()
-        # print self._layout.rowMinimumHeight(0)
-        # print self.size()
-        # print self.minimumSize()
-        # print self.sizeHint()
-        # print self.minimumSizeHint()
-        #pass resize event on to qwidget
-        # call layout()
-        QWidget.resizeEvent(self, event)
-        size = event.size()
-        if size.width() == self.size().width() and size.height() == self.size().height():
-            # print 'relaying out widgets'
-            self._layout_widgets()
-
+        # This method can end up undergoing infinite recursion for some window layouts,
+        # see https://github.com/labscript-suite/labscript-utils/issues/27. It seems
+        # that sometimes self._layout_widgets() increases the number of columns, which
+        # then triggers a resizeEvent, which then calls self._layout_widgets() which
+        # then decreases the number of columns to its previous value, which triggers a
+        # resizeEvent, and so on. That loop may occur e.g. if increasing/decreasing the
+        # number of columns add/removes a scrollbar, which then changes the number of
+        # widgets that can fit in a row. Limit the recursion depth to
+        # self.MAX_RESIZE_RECURSION to avoid this
+        if self._resize_recursion_depth > self.MAX_RESIZE_RECURSION:
+            return
+        self._resize_recursion_depth += 1
+        try:
+            QWidget.resizeEvent(self, event)
+            size = event.size()
+            if size.width() == self.size().width() and size.height() == self.size().height():
+                self._layout_widgets()
+        finally:
+            self._resize_recursion_depth -= 1

 # A simple test!
 if __name__ == '__main__':

@chrisjbillington
Copy link
Member

It sounds like you have thought about this a bit though, and your solution is still quite simple, so if you think it's the right way to go then I'll believe you!

@chrisjbillington
Copy link
Member

Another thought, does moving QWidget.resizeEvent(self, event) to be after the code calling self._layout_widgets() make a difference?

@zakv
Copy link
Contributor Author

zakv commented Feb 8, 2021

Yeah your suggestion with the recursion counter was the first thing I tried and I was very confused when it didn't work. The recursion counter would work for something like this:

MAX_RECURSION = 3
recursion_level = 0

def f():
    global recursion_level
    print(f"Recursion level: {recursion_level}")
    recursion_level += 1
    if recursion_level < MAX_RECURSION:
        f()
    recursion_level -= 1

f()

Which prints what you'd expect:

Recursion level: 0
Recursion level: 1
Recursion level: 2

However, the recursion here works a little differently. It's more like the following:

from queue import Queue

MAX_RECURSION = 3
recursion_level = 0
event_queue = Queue()

def f():
    global recursion_level
    print(f"Recursion level: {recursion_level}")
    recursion_level += 1
    if recursion_level < MAX_RECURSION:
        event_queue.put(f)
    recursion_level -= 1

event_queue.put(f)  # Put in one event to get things started.
while not event_queue.empty():
    next_event_function = event_queue.get()
    next_event_function()

Which just prints the following over and over:

Recursion level: 0
Recursion level: 0
Recursion level: 0
...

The issue with the queue-based recursion is that each call to f() runs to completion before the following recursive call to f() starts. That means that the recursion_level -= 1 line of one call to f() runs before the next call to f() starts, and so recursion_level just goes back and forth between 0 and 1 instead of increasing.

I suspect that the reason blacs crashes is that the equivalent of f() there actually sends multiple resize events to the event queue, and so the event queue grows in each recursive iteration until there's a stack overflow. I haven't explicitly checked that though.

I'll try your other suggestion, namely switching the order of QWidget.resizeEvent(self, event) and self._layout_widgets() and see how it goes. If I understand the issue correctly though I think that with either ordering more resize events get added to the event queue and so the queue never empties.

@zakv
Copy link
Contributor Author

zakv commented Feb 8, 2021

I just tried switching the ordering of QWidget.resizeEvent(self, event) and self._layout_widgets() then opening blacs with one of the problematic hdf5 settings files (without the changes from this PR). It failed to open with either ordering of those commands

@chrisjbillington
Copy link
Member

Thanks Zak. This is different to what I expected, because a stack overflow specifically implies some function is recursing by calling itself, not the queued-event infinite loop you're describing.

But it's true that events are queued - it's only signals that are called immediately with all the re-entrancy that implies. So since we're looking at events and not signals, it makes sense our functions aren't recursing

Perhaps the queue is implemented with actual recursion on the stack somewhere in the Qt internals even though our functions are not re-entering themselves, resulting in the stack overflow that was observed.

Anyway, glad to have it fixed! Thanks again.

@chrisjbillington chrisjbillington merged commit e74472c into labscript-suite:master Feb 8, 2021
@zakv
Copy link
Contributor Author

zakv commented Feb 8, 2021

Ah gotcha. I haven't done a whole lot of work in low-level languages so my understanding of stack/heap isn't all that great. From this method docsting I thought that events sometimes got allocated on the stack but I'm very likely just misunderstanding that. Your explanation about the queue possibly using recursion internally sounds more reasonable.

Anyway thanks for the code review and the merge!

@zakv zakv deleted the fix-27 branch February 12, 2021 06:19
dihm added a commit that referenced this pull request Nov 30, 2021
commit a79d0e4
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Tue Nov 30 11:20:27 2021 -0500

    Update setup.cfg in preparation for release.

commit 811b43d
Merge: b8726ad a3be0db
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Nov 10 17:07:51 2021 -0500

    Merge pull request #81 from chrisjbillington/py36-path-bug

    Fix bug on Python3.6

commit b8726ad
Merge: d309a5f c4dd2a8
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Nov 10 17:06:47 2021 -0500

    Merge pull request #78 from zakv/setup-logging-jupyter

    Make setup_logging() more Jupyter-friendly.

commit d309a5f
Merge: d5dfea7 12db878
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Nov 10 17:06:02 2021 -0500

    Merge pull request #82 from dihm/zlog_port_profile

    Add default zlog port to default profile configuration file.

commit c4dd2a8
Author: Zak V <zakven@mit.edu>
Date:   Tue Nov 9 20:12:19 2021 -0500

    Removed duplicate call to sys.stdout.fileno() in setup_logging.setup_logging().

    Probably best practice to call it just once rather than calling it first to check if it raises an error then again later to actually use the value it returns.

commit 2e38107
Author: Zak V <zakven@mit.edu>
Date:   Tue Nov 9 20:04:37 2021 -0500

    Restructured try/except block in setup_logging.setup_logging() to avoid catching any UnsupportedOperation errors thrown by code other than sys.stdout.fileno().

commit 97e70c7
Author: Zak V <zakven@mit.edu>
Date:   Tue Nov 9 19:56:07 2021 -0500

    Added warning message when setup_logging.setup_logging() cannot send output to stdout and stderr.

commit b711923
Author: Zak V <zakven@mit.edu>
Date:   Mon Jun 7 17:48:41 2021 -0400

    setup_logging.py is now more Jupyter-friendly.

commit d5dfea7
Merge: 9b76dcb b149e19
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Nov 10 08:54:21 2021 -0500

    Merge pull request #74 from zakv/unitconversions-import-bugfix

    Fixed bug in unitconversions._All._import_all()

commit 12db878
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Mon Nov 8 10:20:28 2021 -0500

    Add default zlog port to default profile configuration file.

commit 9b76dcb
Merge: e13c992 e5cf3fb
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Fri Oct 22 17:53:17 2021 -0400

    Merge pull request #80 from dihm/default_labconfig_docs

    Add default labconfig file to docs

commit a3be0db
Author: chrisjbillington <chrisjbillington@gmail.com>
Date:   Tue Sep 21 13:45:13 2021 +1000

    Fix bug on Python3.6, where Path object not able to be treated as a
    string in ConfigParser.

commit e5cf3fb
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Tue Aug 31 15:29:45 2021 -0400

    Create a doc page that displays the current default labconfig.ini file,
    for easy reference.

commit e13c992
Merge: e74472c 461dc9f
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Fri Jul 16 15:12:37 2021 -0400

    Merge pull request #79 from dihm/labscript_utils-docs

    Update docs to match autogenerating templates used in other modules of labscript.

commit 461dc9f
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Thu Jul 15 10:26:41 2021 -0400

    Add docstring coverage test to build.

commit f90a4cc
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Thu Jul 15 10:26:26 2021 -0400

    Update sphinx version.

commit e45107d
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Mon Jul 12 12:57:18 2021 -0400

    Converting docs build to fully automated version based on recursive
    autosummary calls.

    This more closely resembles how docs are built in the other modules,
    but is slightly different in that submodules are also recursively generated.

commit 56d64f7
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Mon Jul 12 09:25:18 2021 -0400

    Update sphinx pins to same versions as rest of suite.

commit e74472c
Merge: f0999f5 c543328
Author: Chris Billington <chrisjbillington@gmail.com>
Date:   Tue Feb 9 08:54:16 2021 +1100

    Merge pull request #76 from zakv/fix-27

    Work around infinite recursion in ToolPalette.resizeEvent()

commit c543328
Author: Zak V <zakven@mit.edu>
Date:   Mon Feb 8 13:30:47 2021 -0500

    Removed some python 2 debugging print statements from toolpalette.py.

commit d9eb80f
Author: Zak V <zakven@mit.edu>
Date:   Sun Feb 7 23:44:47 2021 -0500

    Fixed #27; worked around bug where ToolPalette.resizeEvent() could trigger itself in an infinite resursion loop.

commit f0999f5
Merge: 8371585 7d14b65
Author: Chris Billington <chrisjbillington@gmail.com>
Date:   Mon Jan 4 13:46:54 2021 +1100

    Merge pull request #75 from chrisjbillington/master

    Do not use deprecated set-env command

commit 7d14b65
Author: chrisjbillington <chrisjbillington@gmail.com>
Date:   Mon Jan 4 13:40:14 2021 +1100

    Do not use deprecated set-env command

commit b149e19
Author: Zak V <zakven@mit.edu>
Date:   Tue Nov 24 18:06:11 2020 -0500

    Fixed bug in unitconversions._All._import_all() where self.__all__ wasn't always changed from None to an empty list before appending things to it.
dihm added a commit that referenced this pull request Mar 21, 2022
commit 95965b8
Merge: a88582d f2f0ac7
Author: Chris Billington <chrisjbillington@gmail.com>
Date:   Thu Mar 17 08:02:09 2022 +1100

    Merge pull request #86 from dihm/replace_distutils

    Replace distutils

commit f2f0ac7
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Wed Mar 16 09:43:51 2022 -0400

    Replace `distutils.log` with basic `logging` in `setup.py`.

commit b6b7823
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Wed Mar 16 09:04:46 2022 -0400

    Replace `distutils.sysconfig` with stdlib `sysconfig` in `modulewatcher`.

commit 9840228
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Wed Mar 16 09:04:10 2022 -0400

    Replace `distutils.version.LooseVersion` with `packaging.version.Version` in `ls_zprocess`.

commit a88582d
Merge: 66c68ab 75a7df6
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Thu Mar 3 09:28:57 2022 -0500

    Merge pull request #84 from dihm/generic_freq_conv

    Add generic frequency conversion class

commit 75a7df6
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Feb 9 12:27:17 2022 -0500

    Add generic frequency conversion class that goes between standard SI prefixes from a base of Hz.

commit 66c68ab
Merge: 7a3d58c 27501a3
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Feb 9 10:47:36 2022 -0500

    Merge pull request #83 from dihm/fix_docs_build

    Prevent starting a zlock server when importing `h5_lock` on RTD.

commit 27501a3
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Feb 9 10:34:22 2022 -0500

    Prevent starting a zlock server when importing `h5_lock` on RTD.

commit 7a3d58c
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Feb 2 17:24:26 2022 -0500

    Pin `mistune` to fix doc builds on RTD.

commit 36c3c6a
Author: chrisjbillington <chrisjbillington@gmail.com>
Date:   Fri Jan 7 17:23:48 2022 +1100

    Ensure only ints passed to QFrame.move in splash

    Otherwise this is a TypeError in the latest PyQt5.

commit f4da12e
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Tue Nov 30 13:00:27 2021 -0500

    Triggering build of release branch by undoing unnecessary pin change for pyqtgraph.

commit a79d0e4
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Tue Nov 30 11:20:27 2021 -0500

    Update setup.cfg in preparation for release.

commit 811b43d
Merge: b8726ad a3be0db
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Nov 10 17:07:51 2021 -0500

    Merge pull request #81 from chrisjbillington/py36-path-bug

    Fix bug on Python3.6

commit b8726ad
Merge: d309a5f c4dd2a8
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Nov 10 17:06:47 2021 -0500

    Merge pull request #78 from zakv/setup-logging-jupyter

    Make setup_logging() more Jupyter-friendly.

commit d309a5f
Merge: d5dfea7 12db878
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Nov 10 17:06:02 2021 -0500

    Merge pull request #82 from dihm/zlog_port_profile

    Add default zlog port to default profile configuration file.

commit c4dd2a8
Author: Zak V <zakven@mit.edu>
Date:   Tue Nov 9 20:12:19 2021 -0500

    Removed duplicate call to sys.stdout.fileno() in setup_logging.setup_logging().

    Probably best practice to call it just once rather than calling it first to check if it raises an error then again later to actually use the value it returns.

commit 2e38107
Author: Zak V <zakven@mit.edu>
Date:   Tue Nov 9 20:04:37 2021 -0500

    Restructured try/except block in setup_logging.setup_logging() to avoid catching any UnsupportedOperation errors thrown by code other than sys.stdout.fileno().

commit 97e70c7
Author: Zak V <zakven@mit.edu>
Date:   Tue Nov 9 19:56:07 2021 -0500

    Added warning message when setup_logging.setup_logging() cannot send output to stdout and stderr.

commit b711923
Author: Zak V <zakven@mit.edu>
Date:   Mon Jun 7 17:48:41 2021 -0400

    setup_logging.py is now more Jupyter-friendly.

commit d5dfea7
Merge: 9b76dcb b149e19
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Wed Nov 10 08:54:21 2021 -0500

    Merge pull request #74 from zakv/unitconversions-import-bugfix

    Fixed bug in unitconversions._All._import_all()

commit 12db878
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Mon Nov 8 10:20:28 2021 -0500

    Add default zlog port to default profile configuration file.

commit 9b76dcb
Merge: e13c992 e5cf3fb
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Fri Oct 22 17:53:17 2021 -0400

    Merge pull request #80 from dihm/default_labconfig_docs

    Add default labconfig file to docs

commit a3be0db
Author: chrisjbillington <chrisjbillington@gmail.com>
Date:   Tue Sep 21 13:45:13 2021 +1000

    Fix bug on Python3.6, where Path object not able to be treated as a
    string in ConfigParser.

commit e5cf3fb
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Tue Aug 31 15:29:45 2021 -0400

    Create a doc page that displays the current default labconfig.ini file,
    for easy reference.

commit e13c992
Merge: e74472c 461dc9f
Author: David Meyer <dihm@users.noreply.github.com>
Date:   Fri Jul 16 15:12:37 2021 -0400

    Merge pull request #79 from dihm/labscript_utils-docs

    Update docs to match autogenerating templates used in other modules of labscript.

commit 461dc9f
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Thu Jul 15 10:26:41 2021 -0400

    Add docstring coverage test to build.

commit f90a4cc
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Thu Jul 15 10:26:26 2021 -0400

    Update sphinx version.

commit e45107d
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Mon Jul 12 12:57:18 2021 -0400

    Converting docs build to fully automated version based on recursive
    autosummary calls.

    This more closely resembles how docs are built in the other modules,
    but is slightly different in that submodules are also recursively generated.

commit 56d64f7
Author: David Meyer <dihm.meyer@gmail.com>
Date:   Mon Jul 12 09:25:18 2021 -0400

    Update sphinx pins to same versions as rest of suite.

commit e74472c
Merge: f0999f5 c543328
Author: Chris Billington <chrisjbillington@gmail.com>
Date:   Tue Feb 9 08:54:16 2021 +1100

    Merge pull request #76 from zakv/fix-27

    Work around infinite recursion in ToolPalette.resizeEvent()

commit c543328
Author: Zak V <zakven@mit.edu>
Date:   Mon Feb 8 13:30:47 2021 -0500

    Removed some python 2 debugging print statements from toolpalette.py.

commit d9eb80f
Author: Zak V <zakven@mit.edu>
Date:   Sun Feb 7 23:44:47 2021 -0500

    Fixed #27; worked around bug where ToolPalette.resizeEvent() could trigger itself in an infinite resursion loop.

commit f0999f5
Merge: 8371585 7d14b65
Author: Chris Billington <chrisjbillington@gmail.com>
Date:   Mon Jan 4 13:46:54 2021 +1100

    Merge pull request #75 from chrisjbillington/master

    Do not use deprecated set-env command

commit 7d14b65
Author: chrisjbillington <chrisjbillington@gmail.com>
Date:   Mon Jan 4 13:40:14 2021 +1100

    Do not use deprecated set-env command

commit b149e19
Author: Zak V <zakven@mit.edu>
Date:   Tue Nov 24 18:06:11 2020 -0500

    Fixed bug in unitconversions._All._import_all() where self.__all__ wasn't always changed from None to an empty list before appending things to it.
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

Successfully merging this pull request may close these issues.

ToolPalette._layout_widgets() appears to recurse, causing a stack overflow
2 participants