From f4c363076b29b4971676483b78025cfead683ca9 Mon Sep 17 00:00:00 2001 From: Krishna Subramanian Date: Sat, 14 Dec 2019 02:08:49 -0800 Subject: [PATCH 1/4] Implemented native inputhooks for MacOSX backend Pulled in IPython (7.10.1) code for MacOSX backend. This code contains MacOSX-native inputhooks. It cannot directly be incorporated IPython uses prompt_tooklit for AsyncIO. In order to avoid dependency on prompt_toolkit, wrote a custom event-loop trigger. See details below. Benefits: - The MacOSX event loop is interrupted only on user input. This greatly improves responsiveness of the backend. The backend now feels more fluid and mac-like. - In PyCharm, the figure does not remain in the forefront even after Alt-Tab. Impelementation Details: IPython uses prompt_toolkit trigger mechanism to initiate callback on user input. To avoid the prompt_toolkit dependency, a similar trigger mechanism using pipes is impelemented along with dedicated thread to check for user input (by checking stdin_read()). When user input is available, this thread terminates the event loop using the pipe functionality similar to IPython. Updates --- pydev_ipython/inputhook.py | 20 +--- pydev_ipython/inputhookmac.py | 167 ++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 pydev_ipython/inputhookmac.py diff --git a/pydev_ipython/inputhook.py b/pydev_ipython/inputhook.py index f12b7f732..9cd8efa9b 100644 --- a/pydev_ipython/inputhook.py +++ b/pydev_ipython/inputhook.py @@ -423,24 +423,12 @@ def disable_gtk3(self): def enable_mac(self, app=None): """ Enable event loop integration with MacOSX. - We call function pyplot.pause, which updates and displays active - figure during pause. It's not MacOSX-specific, but it enables to - avoid inputhooks in native MacOSX backend. - Also we shouldn't import pyplot, until user does it. Cause it's - possible to choose backend before importing pyplot for the first - time only. + Uses native inputhooks for MacOSX that significantly improve + performance and responsiveness. + """ - def inputhook_mac(app=None): - if self.pyplot_imported: - pyplot = sys.modules['matplotlib.pyplot'] - try: - pyplot.pause(0.01) - except: - pass - else: - if 'matplotlib.pyplot' in sys.modules: - self.pyplot_imported = True + from pydev_ipython.inputhookmac import inputhook_mac self.set_inputhook(inputhook_mac) self._current_gui = GUI_OSX diff --git a/pydev_ipython/inputhookmac.py b/pydev_ipython/inputhookmac.py new file mode 100644 index 000000000..61ed738b9 --- /dev/null +++ b/pydev_ipython/inputhookmac.py @@ -0,0 +1,167 @@ +"""Inputhook for OS X + +Calls NSApp / CoreFoundation APIs via ctypes. +""" + +import os +from pydev_ipython.inputhook import stdin_ready +import time +from threading import Thread, Event + +# obj-c boilerplate from appnope, used under BSD 2-clause + +import ctypes +import ctypes.util + +objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) + +void_p = ctypes.c_void_p + +objc.objc_getClass.restype = void_p +objc.sel_registerName.restype = void_p +objc.objc_msgSend.restype = void_p +objc.objc_msgSend.argtypes = [void_p, void_p] + +msg = objc.objc_msgSend + +ccounter = True + +def _utf8(s): + """ensure utf8 bytes""" + if not isinstance(s, bytes): + s = s.encode('utf8') + return s + +def n(name): + """create a selector name (for ObjC methods)""" + return objc.sel_registerName(_utf8(name)) + +def C(classname): + """get an ObjC Class by name""" + return objc.objc_getClass(_utf8(classname)) + +# end obj-c boilerplate from appnope + +# CoreFoundation C-API calls we will use: +CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation')) + +CFFileDescriptorCreate = CoreFoundation.CFFileDescriptorCreate +CFFileDescriptorCreate.restype = void_p +CFFileDescriptorCreate.argtypes = [void_p, ctypes.c_int, ctypes.c_bool, void_p] + +CFFileDescriptorGetNativeDescriptor = CoreFoundation.CFFileDescriptorGetNativeDescriptor +CFFileDescriptorGetNativeDescriptor.restype = ctypes.c_int +CFFileDescriptorGetNativeDescriptor.argtypes = [void_p] + +CFFileDescriptorEnableCallBacks = CoreFoundation.CFFileDescriptorEnableCallBacks +CFFileDescriptorEnableCallBacks.restype = None +CFFileDescriptorEnableCallBacks.argtypes = [void_p, ctypes.c_ulong] + +CFFileDescriptorCreateRunLoopSource = CoreFoundation.CFFileDescriptorCreateRunLoopSource +CFFileDescriptorCreateRunLoopSource.restype = void_p +CFFileDescriptorCreateRunLoopSource.argtypes = [void_p, void_p, void_p] + +CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent +CFRunLoopGetCurrent.restype = void_p + +CFRunLoopAddSource = CoreFoundation.CFRunLoopAddSource +CFRunLoopAddSource.restype = None +CFRunLoopAddSource.argtypes = [void_p, void_p, void_p] + +CFRelease = CoreFoundation.CFRelease +CFRelease.restype = None +CFRelease.argtypes = [void_p] + +CFFileDescriptorInvalidate = CoreFoundation.CFFileDescriptorInvalidate +CFFileDescriptorInvalidate.restype = None +CFFileDescriptorInvalidate.argtypes = [void_p] + +# From CFFileDescriptor.h +kCFFileDescriptorReadCallBack = 1 +kCFRunLoopCommonModes = void_p.in_dll(CoreFoundation, 'kCFRunLoopCommonModes') + + +def _NSApp(): + """Return the global NSApplication instance (NSApp)""" + return msg(C('NSApplication'), n('sharedApplication')) + + +def _wake(NSApp): + """Wake the Application""" + event = msg(C('NSEvent'), + n('otherEventWithType:location:modifierFlags:' + 'timestamp:windowNumber:context:subtype:data1:data2:'), + 15, # Type + 0, # location + 0, # flags + 0, # timestamp + 0, # window + None, # context + 0, # subtype + 0, # data1 + 0, # data2 + ) + msg(NSApp, n('postEvent:atStart:'), void_p(event), True) + + +_triggered = Event() + +def _input_callback(fdref, flags, info): + """Callback to fire when there's input to be read""" + + _triggered.set() + CFFileDescriptorInvalidate(fdref) + CFRelease(fdref) + NSApp = _NSApp() + msg(NSApp, n('stop:'), NSApp) + _wake(NSApp) + +_c_callback_func_type = ctypes.CFUNCTYPE(None, void_p, void_p, void_p) +_c_input_callback = _c_callback_func_type(_input_callback) + +def _stop_on_read(fd): + """Register callback to stop eventloop when there's data on fd""" + + _triggered.clear() + fdref = CFFileDescriptorCreate(None, fd, False, _c_input_callback, None) + CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack) + source = CFFileDescriptorCreateRunLoopSource(None, fdref, 0) + loop = CFRunLoopGetCurrent() + CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes) + CFRelease(source) + +class Timer(Thread): + def __init__(self, callback=None, interval=0.1): + super().__init__() + self.callback = callback + self.interval = interval + self.stop = Event() + + def run(self, *args, **kwargs): + if callable(self.callback): + while not self.stop.is_set(): + time.sleep(self.interval) + self.callback(self.stop) + +def inputhook_mac(): + + import datetime + dt = datetime.datetime.utcnow() + + _r, _w = os.pipe() + def cb(stop): + if stdin_ready(): + os.write(_w, b'x') + stop.set() + t = Timer(callback=cb) + t.start() + NSApp = _NSApp() + _stop_on_read(_r) + msg(NSApp, n('run')) + if not _triggered.is_set(): + # app closed without firing callback, + # probably due to last window being closed. + # Run the loop manually in this case, + # since there may be events still to process (#9734) + CoreFoundation.CFRunLoopRun() + t.join() From 1077a8f20b527900b39241e9ed22ca2393ee5674 Mon Sep 17 00:00:00 2001 From: Krishna Subramanian Date: Tue, 17 Dec 2019 12:04:28 -0800 Subject: [PATCH 2/4] Fix file handle leak --- pydev_ipython/inputhookmac.py | 40 +++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/pydev_ipython/inputhookmac.py b/pydev_ipython/inputhookmac.py index 61ed738b9..3ba2c555a 100644 --- a/pydev_ipython/inputhookmac.py +++ b/pydev_ipython/inputhookmac.py @@ -143,25 +143,29 @@ def run(self, *args, **kwargs): time.sleep(self.interval) self.callback(self.stop) -def inputhook_mac(): - import datetime - dt = datetime.datetime.utcnow() +def inputhook_mac(): + rh, wh = os.pipe() - _r, _w = os.pipe() - def cb(stop): + def inputhook_cb(stop): if stdin_ready(): - os.write(_w, b'x') + os.write(wh, b'x') stop.set() - t = Timer(callback=cb) - t.start() - NSApp = _NSApp() - _stop_on_read(_r) - msg(NSApp, n('run')) - if not _triggered.is_set(): - # app closed without firing callback, - # probably due to last window being closed. - # Run the loop manually in this case, - # since there may be events still to process (#9734) - CoreFoundation.CFRunLoopRun() - t.join() + + try: + t = Timer(callback=inputhook_cb) + t.start() + NSApp = _NSApp() + _stop_on_read(rh) + msg(NSApp, n('run')) + if not _triggered.is_set(): + # app closed without firing callback, + # probably due to last window being closed. + # Run the loop manually in this case, + # since there may be events still to process (#9734) + CoreFoundation.CFRunLoopRun() + t.join() + finally: + os.read(rh, 1) + os.close(rh) + os.close(wh) From 42041229ac7bd17935287c9d61947f7a1ed8d65c Mon Sep 17 00:00:00 2001 From: Krishna Subramanian Date: Tue, 17 Dec 2019 16:07:26 -0800 Subject: [PATCH 3/4] Cleaned up implementation after more testing. Implemented a singleton resource manager to avoid creating pipe resources for every event loop. This also plays nicely with the native hooks. In a previous implementation, closing the pipes lead the native callbacks getting triggered immediately. --- pydev_ipython/inputhookmac.py | 64 ++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/pydev_ipython/inputhookmac.py b/pydev_ipython/inputhookmac.py index 3ba2c555a..da575f651 100644 --- a/pydev_ipython/inputhookmac.py +++ b/pydev_ipython/inputhookmac.py @@ -130,42 +130,60 @@ def _stop_on_read(fd): CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes) CFRelease(source) + class Timer(Thread): def __init__(self, callback=None, interval=0.1): super().__init__() self.callback = callback self.interval = interval - self.stop = Event() + self._stopev = Event() def run(self, *args, **kwargs): if callable(self.callback): - while not self.stop.is_set(): + while not self._stopev.is_set(): time.sleep(self.interval) - self.callback(self.stop) + self.callback(self._stopev) + + +class FHSingleton(object): + """Implements a singleton resource manager for pipes. Avoids opening and + closing pipes during event loops. + """ + _instance = None + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls.rh, cls.wh = os.pipe() + else: + # Clears the character written to trigger callback in the last + # loop. + os.read(cls.rh, 1) + + return cls._instance def inputhook_mac(): - rh, wh = os.pipe() + fh = FHSingleton() + # stop_cb is used to cleanly terminate loop when last figure window is + # closed. + stop_cb = Event() def inputhook_cb(stop): - if stdin_ready(): - os.write(wh, b'x') + if stop_cb.is_set() or stdin_ready(): + os.write(fh.wh, b'x') stop.set() - try: - t = Timer(callback=inputhook_cb) - t.start() - NSApp = _NSApp() - _stop_on_read(rh) - msg(NSApp, n('run')) - if not _triggered.is_set(): - # app closed without firing callback, - # probably due to last window being closed. - # Run the loop manually in this case, - # since there may be events still to process (#9734) - CoreFoundation.CFRunLoopRun() - t.join() - finally: - os.read(rh, 1) - os.close(rh) - os.close(wh) + + t = Timer(callback=inputhook_cb) + t.start() + NSApp = _NSApp() + _stop_on_read(fh.rh) + msg(NSApp, n('run')) + if not _triggered.is_set(): + # app closed without firing callback, + # probably due to last window being closed. + # Run the loop manually in this case, + # since there may be events still to process (#9734) + CoreFoundation.CFRunLoopRun() + stop_cb.set() + t.join() From 7f755cabccea3de3461ff2c0beec1d924a85e5a4 Mon Sep 17 00:00:00 2001 From: Krishna Subramanian Date: Wed, 18 Dec 2019 21:37:44 -0800 Subject: [PATCH 4/4] Import Thread and Event from _pydev_imps._pydev_saved_modules.threading gevent monkey-patches threading and this import will make sure that the original (non monkey-patched version) of the module is used. --- pydev_ipython/inputhookmac.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pydev_ipython/inputhookmac.py b/pydev_ipython/inputhookmac.py index da575f651..a67c64cbd 100644 --- a/pydev_ipython/inputhookmac.py +++ b/pydev_ipython/inputhookmac.py @@ -6,7 +6,7 @@ import os from pydev_ipython.inputhook import stdin_ready import time -from threading import Thread, Event +from _pydev_imps._pydev_saved_modules import threading as _threading_ # obj-c boilerplate from appnope, used under BSD 2-clause @@ -104,7 +104,7 @@ def _wake(NSApp): msg(NSApp, n('postEvent:atStart:'), void_p(event), True) -_triggered = Event() +_triggered = _threading_.Event() def _input_callback(fdref, flags, info): """Callback to fire when there's input to be read""" @@ -131,12 +131,12 @@ def _stop_on_read(fd): CFRelease(source) -class Timer(Thread): +class Timer(_threading_.Thread): def __init__(self, callback=None, interval=0.1): super().__init__() self.callback = callback self.interval = interval - self._stopev = Event() + self._stopev = _threading_.Event() def run(self, *args, **kwargs): if callable(self.callback): @@ -167,7 +167,7 @@ def inputhook_mac(): # stop_cb is used to cleanly terminate loop when last figure window is # closed. - stop_cb = Event() + stop_cb = _threading_.Event() def inputhook_cb(stop): if stop_cb.is_set() or stdin_ready(): os.write(fh.wh, b'x')