macOS Rework: Event Loop - #32363
Draft
iccir wants to merge 22 commits into
Draft
macOS Rework: Event Loop#32363iccir wants to merge 22 commits into
iccir wants to merge 22 commits into
Conversation
Obj-C classes: MatplotlibAppDelegate -> MPLLegacyAppDelegate Window -> MPLLegacyWindow View -> MPLLegacyView NavigationToolbar2Handler -> MPLLegacyNavigationToolbar2Handler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR summary
Important
This PR depends on #32161 and will appear larger than it really is until #32161 is merged. To view the actual changes in this PR, compare macos-staging → macos-pr-eventloop.
This PR moves all event loop related logic to a new
MPLEventLoopclass.Closes #30419
Event Loop Overview
Typically, macOS uses the following code template to run the event loop:
macOS provides
-[NSApplication run]. This uses the above template with the following variables:date+[NSDate distantFuture].shouldStop_runningivar. IsNOwhen_runningis 0, andYESwhen_running> 0._runningis typically1under normal conditions or2when the application is running with a modal dialog present.When
-[NSApplication stop:]is called, the private_runningivar is set to 0. The current event will finish processing and the loop will break on the next iteration.Calls to
-[NSApplication run]are not suppose to be re-entrant. A call to-[NSApplication stop:]will break out of all-[NSApplication run]calls on the stack. This was the root cause of #30419.To manually spin up the event loop, the above code template should be replicated with
dateandshouldStopset accordingly. To check the stop condition, a "fake"NSEventshould be posted via-[NSApplication postEvent:atStart:].It's permissible to manually spin up the event loop inside of a call to
-[NSApplication run]as long as there is only one-[NSApplication run]on the call stack. In addition,-[NSApplication run]should always be used when possible as it performs one-time initialization and may contain possible performance improvements.Our Usage
We have four situations where we need to spin up the event loop:
PyOS_InputHookflush_eventsshowstart_event_loopstop_event_loop()or a timeout occurs.showandstart_event_looptend to be the "main" entry points for long-running event loops. It doesn't make sense to call these in a nested fashion. These are good examples of where-[NSApplication run]should be used.flush_eventscan also be a "main" entry point when used for blitting. However, by design, it is not long-running – it stops spinning when the event queue becomes empty. It's easiest to use the above code template manually rather than trying to use-[NSApplication run].PyOS_InputHookcan be called when the event loop is already running. As such, it cannot use-[NSApplication run].MPLEventLoop
The MPLEventLoop singleton manages all interaction with the macOS event loop. It also handles our
PyErr_CheckSignalslogic, which needed a home and is closely related.MPLEventLoop categorizes the above-mentioned situations into "spins" vs. "runs". "Spins" may be called while the event loop is already running. "Runs" call
-[NSApplication run]and will raise an error if the event loop is already running.The exact method calls are as follows:
PyOS_InputHook-spinUntilStandardInputflush_events-spinUntilNoEventsshow-runUntilStopCondition:(stop condition of "no open figures")
start_event_loop-runUntilTimeout:MPLEventLoop keeps track of the call stack and will correctly stop all loops when
-stopis called.Logging
Issues related to the event loop can be notoriously difficult to debug. As such, MPLEventLoop logs all calls with our
os_log_tlogger.To view them, use the following
log stream:log stream --debug --predicate \ 'subsystem == "org.matplotlib" AND message CONTAINS "EventLoop"'Running the example in #30419 results in:
SIGINT Handler Changes
matplotlib provides an
_allow_interruptcontext manager which enables a SIGINT to stop the event loop.This mostly worked in the old macosx backend - we used a
NSFileHandleto listen to the file descriptor and callPyErr_CheckSignals()on activity. However, theNSFileHandleinstance effectively owned itself – it was kept alive by a strong reference in its own callback.This is fixed in the new macos backend by adding an optional
cleanupcallback to_allow_interrupt. The callback runs afterhandle_sigintbut before the socket pair is closed. This specific order allows us to use Apple's libdispatch to monitor the file descriptor rather thanNSFileHandle. During cleanup, we can ensure thatdispatch_cancelis called beforeclose.-[MPLEventLoop updateCheckSignalsFileDescriptor:]handles the setup or teardown of the dispatch source. To match our design, the dispatch source invokesMPLCheckSignals()from MPLUtils rather than usingPyErr_CheckSignals()directly.matplotlib API Changes
show()when the macOS event loop is already running now raises an error.start_event_loop()when the macOS event loop is already running now raises an error.AI Disclosure
PR quality check