From b1822cf873ad7ed10e727548566bd19653e9102a Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Tue, 15 Oct 2024 21:15:14 -0600 Subject: [PATCH 1/4] FIX: macos: remove our custom PyOS_InputHook after our runloop stops We want to defer back to Python's native input hook handling when we don't have any figures to interact with, otherwise our runloop will continue running unecessarily. Additionally, we need to add a short runloop burst in our hotloop to avoid saturating the CPU while waiting for input. (i.e. sitting on an idle figure would keep 100% cpu while waiting for an input() event without this runloop trigger). --- src/_macosx.m | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index 09838eccaf98..3f457e9f0d40 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -88,6 +88,10 @@ static int wait_for_stdin() { if (!event) { break; } [NSApp sendEvent: event]; } + // We need to run the run loop for a short time to allow the + // events to be processed and keep flushing them while we wait for stdin + // without this, the CPU usage will be very high constantly polling this loop + [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.01]]; } // Remove the input handler as an observer [[NSNotificationCenter defaultCenter] removeObserver: stdinHandle]; @@ -192,16 +196,16 @@ void process_event(char const* cls_name, char const* fmt, ...) static bool backend_inited = false; static void lazy_init(void) { + // Run our own event loop while waiting for stdin on the Python side + // this is needed to keep the application responsive while waiting for input + PyOS_InputHook = wait_for_stdin; + if (backend_inited) { return; } backend_inited = true; NSApp = [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; [NSApp setDelegate: [[[MatplotlibAppDelegate alloc] init] autorelease]]; - - // Run our own event loop while waiting for stdin on the Python side - // this is needed to keep the application responsive while waiting for input - PyOS_InputHook = wait_for_stdin; } static PyObject* @@ -236,6 +240,8 @@ static void lazy_init(void) { static PyObject* stop(PyObject* self) { + // Remove our input hook and stop the event loop. + PyOS_InputHook = NULL; [NSApp stop: nil]; // Post an event to trigger the actual stopping. [NSApp postEvent: [NSEvent otherEventWithType: NSEventTypeApplicationDefined @@ -1122,10 +1128,13 @@ - (void)close { [super close]; --FigureWindowCount; - if (!FigureWindowCount) [NSApp stop: self]; - /* This is needed for show(), which should exit from [NSApp run] - * after all windows are closed. - */ + if (!FigureWindowCount) { + /* This is needed for show(), which should exit from [NSApp run] + * after all windows are closed. + */ + PyObject* x = stop(NULL); + Py_DECREF(x); + } // For each new window, we have incremented the manager reference, so // we need to bring that down during close and not just dealloc. Py_DECREF(manager); From 18909c7237cb4ce7dd2d9c9b16e1b94c83791eb9 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 18 Oct 2024 15:52:18 -0500 Subject: [PATCH 2/4] Rework stdin handling on macos objective C --- src/_macosx.m | 64 +++++++++++++++++---------------------------------- 1 file changed, 21 insertions(+), 43 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index 3f457e9f0d40..b43ca1982e06 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -40,22 +40,32 @@ static bool keyChangeCapsLock = false; /* Keep track of the current mouse up/down state for open/closed cursor hand */ static bool leftMouseGrabbing = false; -/* Keep track of whether stdin has been received */ -static bool stdin_received = false; -static bool stdin_sigint = false; // Global variable to store the original SIGINT handler static PyOS_sighandler_t originalSigintAction = NULL; +// Stop the current app's run loop, sending an event to ensure it actually stops +static void stop_with_event() { + [NSApp stop: nil]; + // Post an event to trigger the actual stopping. + [NSApp postEvent: [NSEvent otherEventWithType: NSEventTypeApplicationDefined + location: NSZeroPoint + modifierFlags: 0 + timestamp: 0 + windowNumber: 0 + context: nil + subtype: 0 + data1: 0 + data2: 0] + atStart: YES]; +} + // Signal handler for SIGINT, only sets a flag to exit the run loop static void handleSigint(int signal) { - stdin_sigint = true; + stop_with_event(); } static int wait_for_stdin() { @autoreleasepool { - stdin_received = false; - stdin_sigint = false; - // Set up a SIGINT handler to interrupt the event loop if ctrl+c comes in too originalSigintAction = PyOS_setsig(SIGINT, handleSigint); @@ -66,33 +76,14 @@ static int wait_for_stdin() { [[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification object: stdinHandle queue: [NSOperationQueue mainQueue] // Use the main queue - usingBlock: ^(NSNotification *notification) { - // Mark that input has been received - stdin_received = true; - } + usingBlock: ^(NSNotification *notification) {stop_with_event();} ]; // Wait in the background for anything that happens to stdin [stdinHandle waitForDataInBackgroundAndNotify]; - // continuously run an event loop until the stdin_received flag is set to exit - while (!stdin_received && !stdin_sigint) { - // This loop is similar to the main event loop and flush_events which have - // Py_[BEGIN|END]_ALLOW_THREADS surrounding the loop. - // This should not be necessary here because PyOS_InputHook releases the GIL for us. - while (true) { - NSEvent *event = [NSApp nextEventMatchingMask: NSEventMaskAny - untilDate: [NSDate distantPast] - inMode: NSDefaultRunLoopMode - dequeue: YES]; - if (!event) { break; } - [NSApp sendEvent: event]; - } - // We need to run the run loop for a short time to allow the - // events to be processed and keep flushing them while we wait for stdin - // without this, the CPU usage will be very high constantly polling this loop - [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.01]]; - } + [NSApp run]; + // Remove the input handler as an observer [[NSNotificationCenter defaultCenter] removeObserver: stdinHandle]; @@ -240,20 +231,7 @@ static void lazy_init(void) { static PyObject* stop(PyObject* self) { - // Remove our input hook and stop the event loop. - PyOS_InputHook = NULL; - [NSApp stop: nil]; - // Post an event to trigger the actual stopping. - [NSApp postEvent: [NSEvent otherEventWithType: NSEventTypeApplicationDefined - location: NSZeroPoint - modifierFlags: 0 - timestamp: 0 - windowNumber: 0 - context: nil - subtype: 0 - data1: 0 - data2: 0] - atStart: YES]; + stop_with_event(); Py_RETURN_NONE; } From 0e8baa58c547799f5939b8d2c265098a0b8c1340 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Tue, 29 Oct 2024 18:19:58 -0500 Subject: [PATCH 3/4] Short circuit input hook when no windows are open --- src/_macosx.m | 61 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index b43ca1982e06..d7c7a23c41f5 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -59,12 +59,37 @@ static void stop_with_event() { atStart: YES]; } -// Signal handler for SIGINT, only sets a flag to exit the run loop +// Signal handler for SIGINT, only argument matching for stop_with_event static void handleSigint(int signal) { - stop_with_event(); + stop_with_event(); +} + +// Helper function to flush all events. +// This is needed in some instances to ensure e.g. that windows are properly closed. +// It is used in the input hook as well as wrapped in a version callable from Python. +static void flushEvents() { + while (true) { + NSEvent* event = [NSApp nextEventMatchingMask: NSEventMaskAny + untilDate: [NSDate distantPast] + inMode: NSDefaultRunLoopMode + dequeue: YES]; + if (!event) { + break; + } + [NSApp sendEvent:event]; + } } static int wait_for_stdin() { + // Short circuit if no windows are active + // Rely on Python's input handling to manage CPU usage + // This queries the NSApp, rather than using our FigureWindowCount because that is decremented when events still + // need to be processed to properly close the windows. + if (![[NSApp windows] count]) { + flushEvents(); + return 1; + } + @autoreleasepool { // Set up a SIGINT handler to interrupt the event loop if ctrl+c comes in too originalSigintAction = PyOS_setsig(SIGINT, handleSigint); @@ -72,8 +97,9 @@ static int wait_for_stdin() { // Create an NSFileHandle for standard input NSFileHandle *stdinHandle = [NSFileHandle fileHandleWithStandardInput]; + // Register for data available notifications on standard input - [[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification + id notificationID = [[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification object: stdinHandle queue: [NSOperationQueue mainQueue] // Use the main queue usingBlock: ^(NSNotification *notification) {stop_with_event();} @@ -82,13 +108,16 @@ static int wait_for_stdin() { // Wait in the background for anything that happens to stdin [stdinHandle waitForDataInBackgroundAndNotify]; + // Run the application's event loop, which will be interrupted on stdin or SIGINT [NSApp run]; // Remove the input handler as an observer - [[NSNotificationCenter defaultCenter] removeObserver: stdinHandle]; + [[NSNotificationCenter defaultCenter] removeObserver: notificationID]; + // Restore the original SIGINT handler upon exiting the function PyOS_setsig(SIGINT, originalSigintAction); + return 1; } } @@ -366,20 +395,9 @@ static CGFloat _get_device_scale(CGContextRef cr) // We run the app, matching any events that are waiting in the queue // to process, breaking out of the loop when no events remain and // displaying the canvas if needed. - NSEvent *event; - Py_BEGIN_ALLOW_THREADS - while (true) { - event = [NSApp nextEventMatchingMask: NSEventMaskAny - untilDate: [NSDate distantPast] - inMode: NSDefaultRunLoopMode - dequeue: YES]; - if (!event) { - break; - } - [NSApp sendEvent:event]; - } + flushEvents(); Py_END_ALLOW_THREADS @@ -1106,13 +1124,10 @@ - (void)close { [super close]; --FigureWindowCount; - if (!FigureWindowCount) { - /* This is needed for show(), which should exit from [NSApp run] - * after all windows are closed. - */ - PyObject* x = stop(NULL); - Py_DECREF(x); - } + if (!FigureWindowCount) [NSApp stop: self]; + /* This is needed for show(), which should exit from [NSApp run] + * after all windows are closed. + */ // For each new window, we have incremented the manager reference, so // we need to bring that down during close and not just dealloc. Py_DECREF(manager); From b2eaad18dcf3dc19fd4dce5f60710e1b315b167d Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 30 Oct 2024 13:41:29 -0500 Subject: [PATCH 4/4] Review comments --- src/_macosx.m | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index d7c7a23c41f5..3c9842120547 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -44,7 +44,7 @@ static PyOS_sighandler_t originalSigintAction = NULL; // Stop the current app's run loop, sending an event to ensure it actually stops -static void stop_with_event() { +static void stopWithEvent() { [NSApp stop: nil]; // Post an event to trigger the actual stopping. [NSApp postEvent: [NSEvent otherEventWithType: NSEventTypeApplicationDefined @@ -59,9 +59,9 @@ static void stop_with_event() { atStart: YES]; } -// Signal handler for SIGINT, only argument matching for stop_with_event +// Signal handler for SIGINT, only argument matching for stopWithEvent static void handleSigint(int signal) { - stop_with_event(); + stopWithEvent(); } // Helper function to flush all events. @@ -70,9 +70,9 @@ static void handleSigint(int signal) { static void flushEvents() { while (true) { NSEvent* event = [NSApp nextEventMatchingMask: NSEventMaskAny - untilDate: [NSDate distantPast] - inMode: NSDefaultRunLoopMode - dequeue: YES]; + untilDate: [NSDate distantPast] + inMode: NSDefaultRunLoopMode + dequeue: YES]; if (!event) { break; } @@ -100,9 +100,9 @@ static int wait_for_stdin() { // Register for data available notifications on standard input id notificationID = [[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification - object: stdinHandle - queue: [NSOperationQueue mainQueue] // Use the main queue - usingBlock: ^(NSNotification *notification) {stop_with_event();} + object: stdinHandle + queue: [NSOperationQueue mainQueue] // Use the main queue + usingBlock: ^(NSNotification *notification) {stopWithEvent();} ]; // Wait in the background for anything that happens to stdin @@ -216,16 +216,16 @@ void process_event(char const* cls_name, char const* fmt, ...) static bool backend_inited = false; static void lazy_init(void) { - // Run our own event loop while waiting for stdin on the Python side - // this is needed to keep the application responsive while waiting for input - PyOS_InputHook = wait_for_stdin; - if (backend_inited) { return; } backend_inited = true; NSApp = [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; [NSApp setDelegate: [[[MatplotlibAppDelegate alloc] init] autorelease]]; + + // Run our own event loop while waiting for stdin on the Python side + // this is needed to keep the application responsive while waiting for input + PyOS_InputHook = wait_for_stdin; } static PyObject* @@ -260,7 +260,7 @@ static void lazy_init(void) { static PyObject* stop(PyObject* self) { - stop_with_event(); + stopWithEvent(); Py_RETURN_NONE; } @@ -1126,8 +1126,8 @@ - (void)close --FigureWindowCount; if (!FigureWindowCount) [NSApp stop: self]; /* This is needed for show(), which should exit from [NSApp run] - * after all windows are closed. - */ + * after all windows are closed. + */ // For each new window, we have incremented the manager reference, so // we need to bring that down during close and not just dealloc. Py_DECREF(manager);