Skip to content

Tk backend improvements #17789

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 9 commits into from
Aug 11, 2020
Merged

Conversation

richardsheridan
Copy link
Contributor

@richardsheridan richardsheridan commented Jun 28, 2020

PR Summary

This PR makes changes to the Tk backend to prevent unnecessary recursion into the Tcl event loop, REPL hangs on figure close, and unnecessary draws on resize plus some extras.

Background

I have a project to explore structured concurrency as applied to GUI design. Specifically, I have written a tkinter "app" that hosts trio in guest mode, and then uses callbacks exclusively to launch async functions, which themselves are eventually scheduled to run using Tk.after_idle.

Changes

Without going in to detail, it is easy to wind up in a situation where FigureCanvasTkAgg.draw is called from an async function in such a way that it deadlocks the event loop. Fortunately, there are absolutely no legitimate reasons to invoke Tk.update or tk.update_idletasks while an event loop is running, so this PR removes them entirely.

Other changes are less directly related to my primary motivation but became apparent as I was diagnosing the deadlock issue.

Resize events call FigureCanvasTkAgg.draw in FigureCanvasTkAgg.resize unnecessarily, as FigureCanvasBase.resize_event calls FigureCanvasTk.draw_idle. This PR removes that line, so resizes will be drawn eventually, without hogging the event loop.

NavigationToolbar2Tk has a special destroy method to delete it's tkinter.StringVar attribute named message. It will be destroyed via Tk object tree (master=self) and GCd by Python (only referred to by other self attributes) without this, so this PR removes it outright.

FigureManagerTk has a destroy method that is completely ineffective and also fails to stop the Tk mainloop. This PR rewrites the method entirely using protocol("WM_DELETE_WINDOW",...) rather than bind("<Destroy>",...). (Cherry picked and merged to master in #17802)

FigureCanvasTk is missing methods start_event_loop and stop_event_loop and so relies on FigureCanvasBase pure Python event loop implementations. This PR reimplements them following the example of the Qt and wx backends. However, I can't find any test code or Matplotlib API functions that actually exercise these methods.

Summary

These changes have minor or no user facing effect unless somewhere Tk.after_idle schedules a callback that contains Tk.update_idletasks. Writing this summary, it seems the fix to FigureManagerTk may actually the most user-noticeable (except most users are probably using Qt.) Not knowing why Tk.update_idletasks was sprinkled about in the first place, it is hard to say if this is backward-incompatable; it should be fine. A test to assert that update or update_idletasks is never called when using the Tk backend would be appropriate, as well as a test of the FigureCanvasTk event loop methods, but I can't fathom how the backend tests are constructed, so some help would be appreciated.

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@richardsheridan
Copy link
Contributor Author

One of the update_idletasks calls was added in #9956, fixing a bug without diagnosing the bug origin. The actual issue was failing to make use of protocol. git blame puts the other insertions at 15-16 years ago!

@tacaswell
Copy link
Member

To check my understanding, in the mainloop function at the module level we are using the c-event loop (because we grab managers[0].window.mainloop()?

@tacaswell
Copy link
Member

On linux the code from #9856

%matplotlib tk
import sys
import matplotlib
import matplotlib as mpl
import pylab as plt

print('OS: %s'%sys.platform)
print('matplotlib version: %s'%mpl.__version__)
print('Backend: %s'%matplotlib.get_backend())

for i in range(3):
    plt.figure()
    plt.close()

plt.figure()
plt.plot([0,1],[0,1])
plt.show()

does not crash, but gives me

OS: linux
matplotlib version: 3.3.0rc1.post102+g69e5b9e4f8
Backend: TkAgg

In [3]: can't invoke "event" command: application has been destroyed                                                                                                 
    while executing
"event generate $w <<ThemeChanged>>"
    (procedure "ttk::ThemeChanged" line 6)
    invoked from within
"ttk::ThemeChanged"
can't invoke "event" command: application has been destroyed
    while executing
"event generate $w <<ThemeChanged>>"
    (procedure "ttk::ThemeChanged" line 6)
    invoked from within
"ttk::ThemeChanged"
can't invoke "event" command: application has been destroyed
    while executing
"event generate $w <<ThemeChanged>>"
    (procedure "ttk::ThemeChanged" line 6)
    invoked from within
"ttk::ThemeChanged"

when run from inside of IPython, but no warnings when run as a script which makes me think that the problem is actually coming from the prompt toolkit inputhook (see https://github.com/ipython/ipython/blob/master/IPython/terminal/pt_inputhooks/tk.py to send you down all of the tk related rabbit holes...)? Are we sure that this will not bring back that crash on the mac? On v3.2.2 I get no warnings, but see the closed windows flicker in and out of existence.

@richardsheridan
Copy link
Contributor Author

To check my understanding, in the mainloop function at the module level we are using the c-event loop (because we grab managers[0].window.mainloop()?

That is correct, to my understanding. for consistency "mainloop" should be everywhere and always the _tkinter.tkapp.mainloop method eventually.

Are we sure that this will not bring back that crash on the mac?

Unfortunately, I don't have a mac to test on. Can we get a test going on CI?

#9856

I get no messages like that on windows from python repl or ipython. I do see the blinky root windows on ipython though!

@tacaswell
Copy link
Member

I have access to a mac that I can test on, but down a rabbit hole of figuring out why plt.ion() in the plain-python prompt is broken in 3.3.0rc1 :(

@tacaswell
Copy link
Member

Fortunately, the bug I thought I found is fixed on both 3.3.x and master as of #17764 !

@tacaswell
Copy link
Member

I can run the code and verify that it does not crash but am having issues getting VNC to work so ~95% sure this is OK. If you are feeling motivated, I think another subprocess test would work here, if not I'll try to take care of it.

@jklymak jklymak requested a review from tacaswell July 15, 2020 22:53
@jklymak
Copy link
Member

jklymak commented Jul 15, 2020

@tacaswell, is this still OK?

@QuLogic
Copy link
Member

QuLogic commented Aug 6, 2020

FigureCanvasTk is missing methods start_event_loop and stop_event_loop and so relies on FigureCanvasBase pure Python event loop implementations. This PR reimplements them following the example of the Qt and wx backends. However, I can't find any test code or Matplotlib API functions that actually exercise these methods.

These should be exercised with plt.pause or plt.ginput. The former is covered by test_backends_interactive.py, though the latter you'd have to check yourself interactively, I think.

@QuLogic
Copy link
Member

QuLogic commented Aug 6, 2020

I don't really understand the destroy bit, so if tests are working, it's probably fine. Everything else looks reasonable, though.

@richardsheridan
Copy link
Contributor Author

FYI plt.ginput checks out after c1cffde. Also, for more on the effects of fixing FigureManager destruction, see this comment.

No need to draw if event loop is responsive
message will be destroyed via tk object tree (master=self) and GCd by Python (only referred to by other self attributes)
tkinter has a perfectly good event loop written in C
timeout is given in float seconds according to FigureCanvasBase, but tk needs int milliseconds
start_event_loop semantics are equivalent to update with timeout < 0.001
@richardsheridan
Copy link
Contributor Author

@QuLogic I believe I have addressed your comment with this last commit, my rebase on master might have made that unclear!

Copy link
Member

@QuLogic QuLogic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small test improvements, but LGTM.

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

Successfully merging this pull request may close these issues.

6 participants