Skip to content

FIX: macosx, always put timers on main thread #25553

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 1 commit into from
Mar 28, 2023

Conversation

greglucas
Copy link
Contributor

PR Summary

The main thread is the one that is running when started, so we should put all timers we control on that thread. Otherwise, we need to start and control runloops on the other threads which may or may not be started. This is particularly important for draw_idle() calls that may be sent from a separate thread where work was being done, but the draw should happen on the main thread still.

closes #5675

To test, you can use the example from #5675 with some updates from other comments in there (save to file and run in interactive mode python -i mac-threading-test.py. Previously, the canvas would never be updated and freeze after the fact due to the extra thread waiting to have a runloop started and capture the draw events that were sent. There is a slight concern that we could hang the GUI event loop if we send too many timer events, but this seems to work just fine in practice here when I'm running this example I can change over to the pan tool and still move the canvas even while receiving the events.

import random
import time
import threading
import matplotlib as mpl
import matplotlib.pyplot as plt


class Plot:

    def __init__(self):
        plt.ion()
        self.start = time.time()
        self.xdata = []
        self.ydata = []
        self.running = None
        fig = plt.figure()
        mpl.rcParams["figure.raise_window"] = True
        plt.show()
        self.axis = fig.add_subplot(111)
        self.line, = self.axis.plot([])
        threading.Thread(target=self.worker).start()

    def worker(self):
        for _ in range(50):
            self.xdata.append(len(self.xdata))
            self.ydata.append(random.random())
            self.axis.set_xlim(0, len(self.xdata))
            self.axis.set_ylim(0, max(self.ydata))
            self.line.set_data(self.xdata, self.ydata)
            time.sleep(0.1)
        print(time.time() - self.start)


if __name__ == '__main__':
    Plot()

PR Checklist

Documentation and Tests

  • Has pytest style unit tests (and pytest passes)
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • New plotting related features are documented with examples.

Release Notes

  • New features are marked with a .. versionadded:: directive in the docstring and documented in doc/users/next_whats_new/
  • API changes are marked with a .. versionchanged:: directive in the docstring and documented in doc/api/next_api_changes/
  • Release notes conform with instructions in next_whats_new/README.rst or next_api_changes/README.rst

The main thread is the one that is running when started, so we
should put all timers we control on that thread. Otherwise we
need to start and control runloops on the other threads which may
or may not be started. This is particularly important for draw_idle()
calls that may be sent from a separate thread where work was being
done, but the draw should happen on the main thread still.
@greglucas greglucas added this to the v3.8.0 milestone Mar 26, 2023
@tacaswell
Copy link
Member

This makes sense from a general "how should event loops work" point of view and I tested this manually (worked as advertised).

@tacaswell tacaswell merged commit bac9b03 into matplotlib:main Mar 28, 2023
@greglucas greglucas deleted the macosx-timer-main-thread branch March 28, 2023 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

plt.pause() with threading is extremely slow for MacOSX backend
3 participants