├── src ├── examples │ ├── __init__.py │ ├── dialog.py │ ├── simple.py │ ├── server.py │ ├── slider_demo.py │ └── more_dialogs.py └── wxasync.py ├── notes.txt ├── setup.py ├── LICENSE ├── .github └── workflows │ └── python-publish.yml ├── README.md └── test ├── poc_windows_patch_iocp.py └── test_perfs.py /src/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | 1. Build 2 | 3 | >python setup.py sdist bdist_wheel 4 | 5 | 2. Test 6 | 7 | >twine upload --repository-url https://test.pypi.org/legacy/ dist/* 8 | 9 | => test in a virtualenv. 10 | pip install wxasync==0.2 --index-url https://test.pypi.org/simple/ --no-deps 11 | 12 | 3. Push to prod 13 | 14 | >twine upload dist/* 15 | or 16 | >twine upload --repository-url https://upload.pypi.org/legacy/ dist/* 17 | 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='wxasync', 4 | version='0.49', 5 | description='asyncio for wxpython', 6 | long_description='wxasync it a library for using python 3 asyncio (async/await) with wxpython.', 7 | url='http://github.com/sirk390/wxasync', 8 | author='C.Bodt', 9 | author_email='sirk390@gmail.com', 10 | license='MIT', 11 | package_dir={'': 'src'}, 12 | install_requires=[ 13 | 'wxpython', 14 | ], 15 | python_requires=">=3.7", 16 | py_modules=['wxasync'], 17 | zip_safe=True, 18 | ) 19 | -------------------------------------------------------------------------------- /src/examples/dialog.py: -------------------------------------------------------------------------------- 1 | from wx import TextEntryDialog 2 | from wxasync import AsyncShowDialog, WxAsyncApp 3 | import asyncio 4 | import wx 5 | 6 | async def opendialog(): 7 | """ This functions demonstrate the use of 'AsyncShowDialog' to Show a 8 | any wx.Dialog asynchronously, and wait for the result. 9 | """ 10 | dlg = TextEntryDialog(None, "Please enter some text:") 11 | return_code = await AsyncShowDialog(dlg) 12 | print ("The ReturnCode is %s and you entered '%s'" % (return_code, dlg.GetValue())) 13 | app = wx.App.Get() 14 | app.ExitMainLoop() 15 | 16 | 17 | if __name__ == '__main__': 18 | async def main(): 19 | app = WxAsyncApp() 20 | asyncio.create_task(opendialog()) 21 | await app.MainLoop() 22 | 23 | 24 | asyncio.run(main()) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 C.Bodt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | workflow_dispatch: 13 | 14 | jobs: 15 | deploy: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: '3.x' 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install build 29 | - name: Build package 30 | run: python -m build 31 | - name: Publish package 32 | uses: pypa/gh-action-pypi-publish@release/v1 33 | with: 34 | user: __token__ 35 | password: ${{ secrets.PYPI_API_TOKEN }} 36 | -------------------------------------------------------------------------------- /src/examples/simple.py: -------------------------------------------------------------------------------- 1 | import wx 2 | from wxasync import AsyncBind, WxAsyncApp, StartCoroutine 3 | import asyncio 4 | from asyncio.events import get_event_loop 5 | import time 6 | 7 | 8 | class TestFrame(wx.Frame): 9 | def __init__(self, parent=None): 10 | super(TestFrame, self).__init__(parent) 11 | vbox = wx.BoxSizer(wx.VERTICAL) 12 | button1 = wx.Button(self, label="Submit") 13 | self.edit = wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE) 14 | self.edit_timer = wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE) 15 | vbox.Add(button1, 2, wx.EXPAND|wx.ALL) 16 | vbox.AddStretchSpacer(1) 17 | vbox.Add(self.edit, 1, wx.EXPAND|wx.ALL) 18 | vbox.Add(self.edit_timer, 1, wx.EXPAND|wx.ALL) 19 | self.SetSizer(vbox) 20 | self.Layout() 21 | AsyncBind(wx.EVT_BUTTON, self.async_callback, button1) 22 | StartCoroutine(self.update_clock, self) 23 | 24 | async def async_callback(self, event): 25 | self.edit.SetLabel("Button clicked") 26 | await asyncio.sleep(1) 27 | self.edit.SetLabel("Working") 28 | await asyncio.sleep(1) 29 | self.edit.SetLabel("Completed") 30 | 31 | async def update_clock(self): 32 | while True: 33 | self.edit_timer.SetLabel(time.strftime('%H:%M:%S')) 34 | await asyncio.sleep(0.5) 35 | 36 | 37 | async def main(): 38 | app = WxAsyncApp() 39 | frame = TestFrame() 40 | frame.Show() 41 | app.SetTopWindow(frame) 42 | await app.MainLoop() 43 | 44 | 45 | asyncio.run(main()) 46 | -------------------------------------------------------------------------------- /src/examples/server.py: -------------------------------------------------------------------------------- 1 | """ Example server using asyncio streams 2 | """ 3 | from wxasync import WxAsyncApp, StartCoroutine 4 | import asyncio 5 | import wx 6 | 7 | 8 | class TestFrame(wx.Frame): 9 | def __init__(self, parent=None): 10 | super(TestFrame, self).__init__(parent, title="Server Example") 11 | vbox = wx.BoxSizer(wx.VERTICAL) 12 | self.logctrl = wx.TextCtrl(self, style=wx.TE_MULTILINE|wx.TE_READONLY) 13 | vbox.Add(self.logctrl, 1, wx.EXPAND|wx.ALL) 14 | self.SetSizer(vbox) 15 | self.Layout() 16 | StartCoroutine(self.run_server, self) 17 | 18 | def log(self, text): 19 | self.logctrl.AppendText(text + "\r\n") 20 | 21 | async def handle_connection(self, reader, writer): 22 | while True: 23 | try: 24 | data = await reader.read(100) 25 | if not data: 26 | break 27 | message = data.decode() 28 | addr = writer.get_extra_info('peername') 29 | self.log(f"Received {message!r} from {addr!r}") 30 | self.log(f"Send: {message!r}") 31 | writer.write(data) 32 | await writer.drain() 33 | except ConnectionError: 34 | break 35 | self.log("Close the connection") 36 | writer.close() 37 | 38 | 39 | async def run_server(self): 40 | server = await asyncio.start_server(self.handle_connection, '127.0.0.1', 8888) 41 | 42 | addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets) 43 | self.log(f'Serving on {addrs}') 44 | 45 | async with server: 46 | await server.serve_forever() 47 | 48 | 49 | async def main(): 50 | app = WxAsyncApp() 51 | frame = TestFrame() 52 | frame.Show() 53 | app.SetTopWindow(frame) 54 | await app.MainLoop() 55 | 56 | 57 | asyncio.run(main()) 58 | 59 | -------------------------------------------------------------------------------- /src/examples/slider_demo.py: -------------------------------------------------------------------------------- 1 | import wx 2 | from wxasync import WxAsyncApp, AsyncBind 3 | import asyncio 4 | from asyncio.events import get_event_loop 5 | 6 | SLIDERS_COUNT = 5 # number of sliders to display. 7 | MAX_VALUE = 100 # maximum value of sliders. 8 | DELAY = 0.05 # delay between updating slider positions. 9 | 10 | class TestFrame(wx.Frame): 11 | def __init__(self, parent=None): 12 | super(TestFrame, self).__init__(parent) 13 | 14 | # Add a button. 15 | vbox = wx.BoxSizer(wx.VERTICAL) 16 | self.button = wx.Button(self, label="Press to Run") 17 | vbox.Add(self.button, 1, wx.CENTRE) 18 | 19 | # Add N sliders. 20 | self.sliders = [] 21 | for i in range(SLIDERS_COUNT): 22 | s = wx.Slider(self) 23 | self.sliders.append(s) 24 | vbox.Add(s, 1, wx.EXPAND|wx.ALL) 25 | 26 | # Layout the gui elements in the frame. 27 | self.SetSizer(vbox) 28 | self.Layout() 29 | 30 | # bind an asyncio coroutine to the button event. 31 | AsyncBind(wx.EVT_BUTTON, self.async_slider_demo, self.button) 32 | 33 | async def async_slider_demo(self, event): 34 | # Update button properties. 35 | self.button.Enabled = False 36 | 37 | for i in range(5): 38 | dots = "." * i 39 | label = "Initialising" + dots 40 | self.button.Label = label 41 | # Add a delay so we can see the button properites change. 42 | # (allows the wx MainLoop async coroutine to run) 43 | await asyncio.sleep(0.5) 44 | 45 | # initilase sliders and increment values for each slider. 46 | inc = [] 47 | for i,s in enumerate(self.sliders): 48 | s.Value = 0 49 | inc.append(i+1) 50 | check = 0 51 | 52 | # Update button properties. 53 | self.button.Label = "Running..." 54 | 55 | # Using asyncio.sleep will yield here and return ASAP. 56 | # (allows the wx MainLoop to run and show button changes) 57 | await asyncio.sleep(0) 58 | 59 | # increment and update each slider, if it hasn't finished yet. 60 | while check < SLIDERS_COUNT: 61 | for i, s in enumerate(self.sliders): 62 | if i < check: 63 | continue # skip updating this slider. 64 | val = s.Value + inc[i] 65 | if val > MAX_VALUE: 66 | val = MAX_VALUE 67 | inc[i] = -inc[i] # change direction (decrease) 68 | elif val < 0: 69 | val = 0 70 | inc[i] = -inc[i] # change direction (increase) 71 | s.Value = val 72 | 73 | # check next slider if current slider has finished. 74 | if self.sliders[check].Value == MAX_VALUE: 75 | check += 1 76 | 77 | # async sleep will delay and yield this async coroutine. 78 | # (allows the wx MainLoop async coroutine to run) 79 | await asyncio.sleep(DELAY) 80 | 81 | # Update button properties. 82 | self.button.Label = "Run again" 83 | self.button.Enabled = True 84 | 85 | if __name__ == '__main__': 86 | 87 | async def main(): 88 | app = WxAsyncApp() 89 | frame = TestFrame() 90 | frame.Show() 91 | app.SetTopWindow(frame) 92 | await app.MainLoop() 93 | 94 | 95 | asyncio.run(main()) 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wxasync 2 | ## asyncio support for wxpython 3 | 4 | wxasync it a library for using python 3 asyncio (async/await) with wxpython. 5 | The library polls UI messages every 20ms and runs the asyncio message loop the rest of the time. 6 | When idle, the CPU usage is 0% on windows and about 1-2% on MacOS. 7 | 8 | ### Installation 9 | wxasync requires python 3.7. For earlier versions of python use wxasync 0.45. 10 | 11 | Install using: 12 | ```sh 13 | pip install wxasync 14 | ``` 15 | ### Usage 16 | Create a **WxAsyncApp** instead of a **wx.App** 17 | 18 | ```python 19 | app = WxAsyncApp() 20 | ``` 21 | 22 | and use **AsyncBind** to bind an event to a coroutine. 23 | ```python 24 | async def async_callback(): 25 | (...your code...) 26 | 27 | AsyncBind(wx.EVT_BUTTON, async_callback, button1) 28 | ``` 29 | You can still use wx.Bind together with AsyncBind. 30 | 31 | If you don't want to wait for an event, you just use **StartCoroutine** and it will be executed immediatly. 32 | It will return an asyncio.Task in case you need to cancel it. 33 | ``` 34 | task = StartCoroutine(update_clock_coroutine, frame) 35 | ``` 36 | If you need to stop it run: 37 | ``` 38 | task.cancel() 39 | ``` 40 | Any coroutine started using **AsyncBind** or using **StartCoroutine** is attached to a wx.Window. It is automatically cancelled when the Window is destroyed. This makes it easier to use, as you don't need to take care of cancelling them yourselve. 41 | 42 | To show a Dialog, use **AsyncShowDialog** or **AsyncShowDialogModal**. This allows 43 | to use 'await' to wait until the dialog completes. Don't use dlg.ShowModal() directly as it would block the event loop. 44 | 45 | You start the application using: 46 | ```python 47 | await app.MainLoop() 48 | ``` 49 | 50 | Below is full example with AsyncBind, WxAsyncApp, and StartCoroutine: 51 | 52 | ```python 53 | import wx 54 | from wxasync import AsyncBind, WxAsyncApp, StartCoroutine 55 | import asyncio 56 | import time 57 | 58 | 59 | class TestFrame(wx.Frame): 60 | def __init__(self, parent=None): 61 | super(TestFrame, self).__init__(parent) 62 | vbox = wx.BoxSizer(wx.VERTICAL) 63 | button1 = wx.Button(self, label="Submit") 64 | self.edit = wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE) 65 | self.edit_timer = wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE) 66 | vbox.Add(button1, 2, wx.EXPAND|wx.ALL) 67 | vbox.AddStretchSpacer(1) 68 | vbox.Add(self.edit, 1, wx.EXPAND|wx.ALL) 69 | vbox.Add(self.edit_timer, 1, wx.EXPAND|wx.ALL) 70 | self.SetSizer(vbox) 71 | self.Layout() 72 | AsyncBind(wx.EVT_BUTTON, self.async_callback, button1) 73 | StartCoroutine(self.update_clock, self) 74 | 75 | async def async_callback(self, event): 76 | self.edit.SetLabel("Button clicked") 77 | await asyncio.sleep(1) 78 | self.edit.SetLabel("Working") 79 | await asyncio.sleep(1) 80 | self.edit.SetLabel("Completed") 81 | 82 | async def update_clock(self): 83 | while True: 84 | self.edit_timer.SetLabel(time.strftime('%H:%M:%S')) 85 | await asyncio.sleep(0.5) 86 | 87 | 88 | async def main(): 89 | app = WxAsyncApp() 90 | frame = TestFrame() 91 | frame.Show() 92 | app.SetTopWindow(frame) 93 | await app.MainLoop() 94 | 95 | 96 | asyncio.run(main()) 97 | 98 | ``` 99 | 100 | ## Performance 101 | 102 | Below is view of the performances (on windows Core I7-7700K 4.2Ghz): 103 | 104 | | Scenario |Latency | Latency (at max throughput)| Max Throughput(msg/s) | 105 | | ------------- |--------------|---------------------------------|-------------| 106 | | asyncio only (for reference) |0ms |17ms |571 325| 107 | | wx only (for reference) |0ms |19ms |94 591| 108 | | wxasync (GUI) | 5ms |19ms |52 304| 109 | | wxasync (GUI+asyncio)| 5ms GUI / 0ms asyncio |24ms GUI / 12ms asyncio |40 302 GUI + 134 000 asyncio| 110 | 111 | 112 | The performance tests are included in the 'test' directory. 113 | -------------------------------------------------------------------------------- /src/wxasync.py: -------------------------------------------------------------------------------- 1 | from asyncio.events import get_event_loop 2 | import asyncio 3 | import wx 4 | import wx.html 5 | import warnings 6 | from asyncio import CancelledError 7 | from collections import defaultdict 8 | import platform 9 | from asyncio.locks import Event 10 | from asyncio.coroutines import iscoroutinefunction 11 | import asyncio 12 | 13 | 14 | IS_MAC = platform.system() == "Darwin" 15 | 16 | class WxAsyncApp(wx.App): 17 | def __init__(self, warn_on_cancel_callback=False, sleep_duration=0.02, **kwargs): 18 | self.BoundObjects = {} 19 | self.RunningTasks = defaultdict(set) 20 | self.exiting = False 21 | self.ui_idle = True 22 | self.sleep_duration = sleep_duration 23 | self.warn_on_cancel_callback = warn_on_cancel_callback 24 | super(WxAsyncApp, self).__init__(**kwargs) 25 | self.SetExitOnFrameDelete(True) 26 | 27 | async def MainLoop(self): 28 | # inspired by https://github.com/wxWidgets/Phoenix/blob/master/samples/mainloop/mainloop.py 29 | evtloop = wx.GUIEventLoop() 30 | with wx.EventLoopActivator(evtloop): 31 | while not self.exiting: 32 | if IS_MAC: 33 | # evtloop.Pending() just returns True on MacOs 34 | evtloop.DispatchTimeout(0) 35 | self.ui_idle = False 36 | else: 37 | while evtloop.Pending(): 38 | evtloop.Dispatch() 39 | await asyncio.sleep(0) 40 | self.ui_idle = False 41 | await asyncio.sleep(self.sleep_duration) 42 | self.ProcessPendingEvents() 43 | if not self.ui_idle: 44 | evtloop.ProcessIdle() 45 | self.ui_idle = True 46 | self.exiting = False 47 | self.OnExit() 48 | 49 | def ExitMainLoop(self): 50 | self.exiting = True 51 | 52 | def AsyncBind(self, event_binder, async_callback, object, source=None, id=wx.ID_ANY, id2=wx.ID_ANY): 53 | """Bind a coroutine to a wx Event. Note that when wx object is destroyed, any coroutine still running will be cancelled automatically. 54 | """ 55 | # We restrict the object to wx.Windows to be able to cancel the coroutines on EVT_WINDOW_DESTROY, even if wx.Bind works with any wx.EvtHandler 56 | if not isinstance(object, wx.Window): 57 | raise Exception("object must be a wx.Window") 58 | if not iscoroutinefunction(async_callback): 59 | raise Exception("async_callback is not a coroutine function") 60 | if object not in self.BoundObjects: 61 | self.BoundObjects[object] = defaultdict(list) 62 | object.Bind(wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, object), object) 63 | self.BoundObjects[object][event_binder.typeId].append(async_callback) 64 | object.Bind(event_binder, lambda event: StartCoroutine(async_callback(event.Clone()), object), source=source, id=id, id2=id2) 65 | 66 | def StartCoroutine(self, coroutine, obj): 67 | """Start and attach a coroutine to a wx object. When object is destroyed, the coroutine will be cancelled automatically. 68 | returns an asyncio.Task 69 | """ 70 | # We restrict the object to wx.Windows to be able to cancel the coroutines on EVT_WINDOW_DESTROY, even if wx.Bind works with any wx.EvtHandler 71 | if not isinstance(obj, wx.Window): 72 | raise Exception("obj must be a wx.Window") 73 | if asyncio.iscoroutinefunction(coroutine): 74 | coroutine = coroutine() 75 | if obj not in self.BoundObjects: 76 | self.BoundObjects[obj] = defaultdict(list) 77 | obj.Bind(wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, obj), obj) 78 | task = asyncio.create_task(coroutine) 79 | task.add_done_callback(self.OnTaskCompleted) 80 | task.obj = obj 81 | self.RunningTasks[obj].add(task) 82 | return task 83 | 84 | def OnTaskCompleted(self, task): 85 | try: 86 | # This gathers completed callbacks (otherwise asyncio will show a warning) 87 | # Note: exceptions from callbacks raise here 88 | # we just let them bubble as there is nothing we can do at this point 89 | _res = task.result() 90 | except CancelledError: 91 | # Cancelled because the window was destroyed, this is normal so ignore it 92 | pass 93 | self.RunningTasks[task.obj].remove(task) 94 | 95 | def OnDestroy(self, event, obj): 96 | # Cancel async callbacks 97 | for task in self.RunningTasks[obj]: 98 | if not task.done(): 99 | task.cancel() 100 | if self.warn_on_cancel_callback: 101 | warnings.warn("cancelling callback" + str(obj) + str(task)) 102 | del self.BoundObjects[obj] 103 | 104 | 105 | def AsyncBind(event, async_callback, object, source=None, id=wx.ID_ANY, id2=wx.ID_ANY): 106 | app = wx.App.Get() 107 | if not isinstance(app, WxAsyncApp): 108 | raise Exception("Create a 'WxAsyncApp' first") 109 | app.AsyncBind(event, async_callback, object, source=source, id=id, id2=id2) 110 | 111 | 112 | def StartCoroutine(coroutine, obj): 113 | app = wx.App.Get() 114 | if not isinstance(app, WxAsyncApp): 115 | raise Exception("Create a 'WxAsyncApp' first") 116 | return app.StartCoroutine(coroutine, obj) 117 | 118 | 119 | # 120 | # Note: os level dialogs like wx.FileDialog, wx.DirDialog, wx.FontDialog, wx.ColourDialog, wx.MessageDialog are 121 | # handled differently: 122 | # * They only support ShowModal 123 | # * They must be run in an executor to avoid blocking the main event loop 124 | # 125 | 126 | 127 | async def ShowModalInExecutor(dlg): 128 | loop = asyncio.get_running_loop() 129 | return await loop.run_in_executor(None, dlg.ShowModal) 130 | 131 | 132 | async def AsyncShowDialog(dlg): 133 | if type(dlg) in [wx.FileDialog, wx.DirDialog, wx.FontDialog, wx.ColourDialog, wx.MessageDialog]: 134 | raise Exception("This type of dialog cannot be shown modless, please use 'AsyncShowDialogModal'") 135 | closed = Event() 136 | def end_dialog(return_code): 137 | dlg.SetReturnCode(return_code) 138 | dlg.Hide() 139 | closed.set() 140 | async def on_button(event): 141 | # Same code as in wxwidgets:/src/common/dlgcmn.cpp:OnButton 142 | # to automatically handle OK, CANCEL, APPLY,... buttons 143 | id = event.GetId() 144 | if id == dlg.GetAffirmativeId(): 145 | if dlg.Validate() and dlg.TransferDataFromWindow(): 146 | end_dialog(id) 147 | elif id == wx.ID_APPLY: 148 | if dlg.Validate(): 149 | dlg.TransferDataFromWindow() 150 | elif id == dlg.GetEscapeId() or (id == wx.ID_CANCEL and dlg.GetEscapeId() == wx.ID_ANY): 151 | end_dialog(wx.ID_CANCEL) 152 | else: 153 | event.Skip() 154 | async def on_close(event): 155 | closed.set() 156 | dlg.Hide() 157 | AsyncBind(wx.EVT_CLOSE, on_close, dlg) 158 | AsyncBind(wx.EVT_BUTTON, on_button, dlg) 159 | dlg.Show() 160 | await closed.wait() 161 | return dlg.GetReturnCode() 162 | 163 | 164 | async def AsyncShowDialogModal(dlg): 165 | if type(dlg) in [wx.html.HtmlHelpDialog, wx.FileDialog, wx.DirDialog, wx.FontDialog, wx.ColourDialog, wx.MessageDialog]: 166 | return await ShowModalInExecutor(dlg) 167 | else: 168 | frames = set(wx.GetTopLevelWindows()) - set([dlg]) 169 | states = {frame: frame.IsEnabled() for frame in frames} 170 | try: 171 | for frame in frames: 172 | frame.Disable() 173 | return await AsyncShowDialog(dlg) 174 | finally: 175 | for frame in frames: 176 | frame.Enable(states[frame]) 177 | parent = dlg.GetParent() 178 | if parent: 179 | parent.SetFocus() 180 | 181 | -------------------------------------------------------------------------------- /src/examples/more_dialogs.py: -------------------------------------------------------------------------------- 1 | import wx 2 | from wxasync import WxAsyncApp, AsyncBind, StartCoroutine, AsyncShowDialog, AsyncShowDialogModal 3 | from asyncio import get_event_loop 4 | import asyncio 5 | import time 6 | from wx._core import TextEntryDialog, FontDialog, MessageDialog, DefaultPosition, DefaultSize 7 | from wx._html import HtmlHelpDialog, HF_DEFAULT_STYLE 8 | from wx._adv import PropertySheetDialog 9 | 10 | 11 | class TestFrame(wx.Frame): 12 | def __init__(self, parent=None): 13 | super(TestFrame, self).__init__(parent, size=(600,800)) 14 | vbox = wx.BoxSizer(wx.VERTICAL) 15 | button1 = wx.Button(self, label="ColourDialog") 16 | button2 = wx.Button(self, label="DirDialog") 17 | button3 = wx.Button(self, label="FileDialog") 18 | # FindReplaceDialog is always modless 19 | button5 = wx.Button(self, label="FontDialog") 20 | # GenericProgressDialog does not input data 21 | button7 = wx.Button(self, label="HtmlHelpDialog") 22 | button8 = wx.Button(self, label="MessageDialog") 23 | button9 = wx.Button(self, label="MultiChoiceDialog") 24 | button10 = wx.Button(self, label="NumberEntryDialog") 25 | button12 = wx.Button(self, label="PrintAbortDialog") 26 | button13 = wx.Button(self, label="PropertySheetDialog") 27 | button14 = wx.Button(self, label="RearrangeDialog") 28 | button16 = wx.Button(self, label="SingleChoiceDialog") 29 | button18 = wx.Button(self, label="TextEntryDialog") 30 | 31 | self.edit_timer = wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE) 32 | vbox.Add(button1, 2, wx.EXPAND|wx.ALL) 33 | vbox.Add(button2, 2, wx.EXPAND|wx.ALL) 34 | vbox.Add(button3, 2, wx.EXPAND|wx.ALL) 35 | vbox.Add(button5, 2, wx.EXPAND|wx.ALL) 36 | vbox.Add(button7, 2, wx.EXPAND|wx.ALL) 37 | vbox.Add(button8, 2, wx.EXPAND|wx.ALL) 38 | vbox.Add(button9, 2, wx.EXPAND|wx.ALL) 39 | vbox.Add(button10, 2, wx.EXPAND|wx.ALL) 40 | vbox.Add(button12, 2, wx.EXPAND|wx.ALL) 41 | vbox.Add(button13, 2, wx.EXPAND|wx.ALL) 42 | vbox.Add(button14, 2, wx.EXPAND|wx.ALL) 43 | vbox.Add(button16, 2, wx.EXPAND|wx.ALL) 44 | vbox.Add(button18, 2, wx.EXPAND|wx.ALL) 45 | 46 | AsyncBind(wx.EVT_BUTTON, self.on_ColourDialog, button1) 47 | AsyncBind(wx.EVT_BUTTON, self.on_DirDialog, button2) 48 | AsyncBind(wx.EVT_BUTTON, self.on_FileDialog, button3) 49 | AsyncBind(wx.EVT_BUTTON, self.on_FontDialog, button5) 50 | AsyncBind(wx.EVT_BUTTON, self.on_HtmlHelpDialog, button7) 51 | AsyncBind(wx.EVT_BUTTON, self.on_MessageDialog, button8) 52 | AsyncBind(wx.EVT_BUTTON, self.on_MultiChoiceDialog, button9) 53 | AsyncBind(wx.EVT_BUTTON, self.on_NumberEntryDialog, button10) 54 | AsyncBind(wx.EVT_BUTTON, self.on_PrintAbortDialog, button12) 55 | AsyncBind(wx.EVT_BUTTON, self.on_PropertySheetDialog, button13) 56 | AsyncBind(wx.EVT_BUTTON, self.on_RearrangeDialog, button14) 57 | AsyncBind(wx.EVT_BUTTON, self.on_SingleChoiceDialog, button16) 58 | AsyncBind(wx.EVT_BUTTON, self.on_TextEntryDialog, button18) 59 | 60 | 61 | button5 = wx.Button(self, label="FontDialog") 62 | button7 = wx.Button(self, label="HtmlHelpDialog") 63 | button8 = wx.Button(self, label="MessageDialog") 64 | button9 = wx.Button(self, label="MultiChoiceDialog") 65 | button10 = wx.Button(self, label="NumberEntryDialog") 66 | button12 = wx.Button(self, label="PrintAbortDialog") 67 | button13 = wx.Button(self, label="PropertySheetDialog") 68 | button14 = wx.Button(self, label="RearrangeDialog") 69 | button16 = wx.Button(self, label="SingleChoiceDialog") 70 | button18 = wx.Button(self, label="TextEntryDialog") 71 | 72 | self.SetSizer(vbox) 73 | self.Layout() 74 | vbox.AddStretchSpacer(1) 75 | vbox.Add(self.edit_timer, 1, wx.EXPAND|wx.ALL) 76 | self.SetSizer(vbox) 77 | self.Layout() 78 | StartCoroutine(self.update_clock, self) 79 | 80 | async def ShowDialog(self, dlg): 81 | response = await AsyncShowDialogModal(dlg) 82 | NAMES = {wx.ID_OK : "ID_OK", wx.ID_CANCEL: "ID_CANCEL"} 83 | print (NAMES.get(response, response)) 84 | return response 85 | 86 | async def update_clock(self): 87 | while True: 88 | self.edit_timer.SetLabel(time.strftime('%H:%M:%S')) 89 | await asyncio.sleep(0.5) 90 | 91 | async def on_ColourDialog(self, event): 92 | data = wx.ColourData() 93 | data.SetColour(wx.BLACK) 94 | dlg = wx.ColourDialog(self, data) 95 | await self.ShowDialog(dlg) 96 | 97 | async def on_DirDialog(self, event): 98 | dlg = wx.DirDialog (None, "Choose input directory", "", wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST) 99 | await self.ShowDialog(dlg) 100 | 101 | async def on_FileDialog(self, event): 102 | dlg = wx.FileDialog(self, "Save XYZ file", wildcard="XYZ files (*.xyz)|*.xyz", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) 103 | await self.ShowDialog(dlg) 104 | 105 | async def on_FontDialog(self, event): 106 | dlg = FontDialog(self) 107 | await self.ShowDialog(dlg) 108 | 109 | async def on_HtmlHelpDialog(self, event): 110 | dlg = HtmlHelpDialog(self, id=wx.ID_ANY, title="", style=HF_DEFAULT_STYLE, data=None) 111 | await self.ShowDialog(dlg) 112 | 113 | async def on_MessageDialog(self, event): 114 | dlg = MessageDialog(self, "message", caption="Caption", style=wx.OK|wx.CENTRE, pos=wx.DefaultPosition) 115 | await self.ShowDialog(dlg) 116 | 117 | async def on_MultiChoiceDialog(self, event): 118 | dlg = wx.MultiChoiceDialog(self, "message", "caption", ["One", "Two", "Three"], style=wx.CHOICEDLG_STYLE, pos=wx.DefaultPosition) 119 | await self.ShowDialog(dlg) 120 | 121 | async def on_NumberEntryDialog(self, event): 122 | dlg = wx.NumberEntryDialog(self, "message", "prompt", "caption", 100, 0, 1000, pos=DefaultPosition) 123 | await self.ShowDialog(dlg) 124 | 125 | async def on_PrintAbortDialog(self, event): 126 | dlg = wx.PrintAbortDialog(self, "documentTitle", pos=DefaultPosition, size=DefaultSize, style=wx.DEFAULT_DIALOG_STYLE, name="dialog") 127 | await self.ShowDialog(dlg) 128 | 129 | async def on_PropertySheetDialog(self, event): 130 | dlg = PropertySheetDialog(self, id=wx.ID_ANY, title="", pos=DefaultPosition, size=DefaultSize, style=wx.DEFAULT_DIALOG_STYLE, name="DialogNameStr") 131 | await self.ShowDialog(dlg) 132 | 133 | async def on_RearrangeDialog(self, event): 134 | dlg = wx.RearrangeDialog(None, 135 | "You can also uncheck the items you don't like " 136 | "at all.", 137 | "Sort the items in order of preference", 138 | [3, 0, 1, 2], ["meat", "fish", "fruits", "beer"]) 139 | await self.ShowDialog(dlg) 140 | 141 | async def on_SingleChoiceDialog(self, event): 142 | dlg = wx.SingleChoiceDialog(self, "message", "caption", ["One", "Two", "Three"], style=wx.CHOICEDLG_STYLE, pos=wx.DefaultPosition) 143 | await self.ShowDialog(dlg) 144 | 145 | async def on_TextEntryDialog(self, event): 146 | dlg = TextEntryDialog(self, "Please enter some text:") 147 | return_code = await self.ShowDialog(dlg) 148 | print ("The ReturnCode is %s and you entered '%s'" % (return_code, dlg.GetValue())) 149 | 150 | 151 | if __name__ == "__main__": 152 | 153 | async def main(): 154 | app = WxAsyncApp() 155 | frame = TestFrame() 156 | frame.Show() 157 | app.SetTopWindow(frame) 158 | await app.MainLoop() 159 | 160 | asyncio.run(main()) 161 | -------------------------------------------------------------------------------- /test/poc_windows_patch_iocp.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a proof of concept to patch the ProactorEventLoop under windows and use 3 | MsgWaitForMultipleObjectsEx instead of GetQueuedCompletionStatus. 4 | This should improve latency and performances for GUI messages 5 | 6 | Conclusion: It works but it is actually slower. 7 | """ 8 | 9 | from wxasync import AsyncBind, WxAsyncApp 10 | import wx 11 | import asyncio 12 | from asyncio.events import get_event_loop 13 | from wx.lib.newevent import NewEvent 14 | import time 15 | from collections import deque 16 | import json 17 | from multiprocessing import Process 18 | import multiprocessing 19 | import itertools 20 | import ctypes 21 | 22 | from asyncio.events import get_event_loop 23 | import asyncio 24 | import wx 25 | import warnings 26 | from asyncio.futures import CancelledError 27 | from collections import defaultdict 28 | import platform 29 | 30 | 31 | TestEvent, EVT_TEST_EVENT = NewEvent() 32 | 33 | 34 | IS_MAC = platform.system() == "Darwin" 35 | 36 | class WxAsyncApp(wx.App): 37 | def __init__(self, warn_on_cancel_callback=False, loop=None): 38 | super(WxAsyncApp, self).__init__() 39 | self.loop = loop or get_event_loop() 40 | self.BoundObjects = {} 41 | self.RunningTasks = defaultdict(set) 42 | self.SetExitOnFrameDelete(True) 43 | self.exiting = asyncio.Event() 44 | self.warn_on_cancel_callback = warn_on_cancel_callback 45 | self.evtloop = wx.GUIEventLoop() 46 | self.activator = wx.EventLoopActivator(self.evtloop) 47 | self.activator.__enter__() 48 | 49 | def DispatchMessages(self): 50 | c = 0 51 | while self.evtloop.Pending(): 52 | c += 1 53 | self.evtloop.Dispatch() 54 | print ("d", c) 55 | self.evtloop.ProcessIdle() 56 | 57 | async def MainLoop(self): 58 | await self.exiting.wait() 59 | 60 | def ExitMainLoop(self): 61 | self.exiting.set() 62 | 63 | def AsyncBind(self, event_binder, async_callback, object): 64 | """Bind a coroutine to a wx Event. Note that when wx object is destroyed, any coroutine still running will be cancelled automatically. 65 | """ 66 | if object not in self.BoundObjects: 67 | self.BoundObjects[object] = defaultdict(list) 68 | object.Bind(wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, object)) 69 | self.BoundObjects[object][event_binder.typeId].append(async_callback) 70 | object.Bind(event_binder, lambda event: self.OnEvent(event, object, event_binder.typeId)) 71 | 72 | def StartCoroutine(self, coroutine, obj): 73 | """Start and attach a coroutine to a wx object. When object is destroyed, the coroutine will be cancelled automatically. 74 | """ 75 | if asyncio.iscoroutinefunction(coroutine): 76 | coroutine = coroutine() 77 | task = self.loop.create_task(coroutine) 78 | task.add_done_callback(self.OnTaskCompleted) 79 | task.obj = obj 80 | self.RunningTasks[obj].add(task) 81 | 82 | def OnEvent(self, event, obj, type): 83 | for asyncallback in self.BoundObjects[obj][type]: 84 | self.StartCoroutine(asyncallback(event.Clone()), obj) 85 | 86 | def OnTaskCompleted(self, task): 87 | try: 88 | # This gathers completed callbacks (otherwise asyncio will show a warning) 89 | # Note: exceptions from callbacks raise here 90 | # we just let them bubble as there is nothing we can do at this point 91 | _res = task.result() 92 | except CancelledError: 93 | # Cancelled because the window was destroyed, this is normal so ignore it 94 | pass 95 | self.RunningTasks[task.obj].remove(task) 96 | 97 | def OnDestroy(self, event, obj): 98 | # Cancel async callbacks 99 | for task in self.RunningTasks[obj]: 100 | if not task.done(): 101 | task.cancel() 102 | if self.warn_on_cancel_callback: 103 | warnings.warn("cancelling callback" + str(obj) + str(task)) 104 | del self.BoundObjects[obj] 105 | 106 | 107 | def AsyncBind(event, async_callback, obj): 108 | app = wx.App.Get() 109 | if type(app) is not WxAsyncApp: 110 | raise Exception("Create a 'WxAsyncApp' first") 111 | app.AsyncBind(event, async_callback, obj) 112 | 113 | 114 | def StartCoroutine(coroutine, obj): 115 | app = wx.App.Get() 116 | if type(app) is not WxAsyncApp: 117 | raise Exception("Create a 'WxAsyncApp' first") 118 | app.StartCoroutine(coroutine, obj) 119 | 120 | 121 | 122 | class TestFrame(wx.Frame): 123 | def __init__(self, parent=None): 124 | super(TestFrame, self).__init__(parent) 125 | vbox = wx.BoxSizer(wx.VERTICAL) 126 | button1 = wx.Button(self, label="Submit") 127 | self.edit = wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE) 128 | vbox.Add(button1, 2, wx.EXPAND|wx.ALL) 129 | vbox.AddStretchSpacer(1) 130 | vbox.Add(self.edit, 1, wx.EXPAND|wx.ALL) 131 | self.SetSizer(vbox) 132 | self.Layout() 133 | 134 | AsyncBind(wx.EVT_BUTTON, self.async_callback, button1) 135 | AsyncBind(wx.EVT_BUTTON, self.async_callback2, button1) 136 | StartCoroutine(self.async_job, self) 137 | 138 | async def async_callback(self, event): 139 | self.edit.SetLabel("Button clicked") 140 | await asyncio.sleep(1) 141 | self.edit.SetLabel("Working") 142 | await asyncio.sleep(1) 143 | self.edit.SetLabel("Completed") 144 | 145 | async def async_callback2(self, event): 146 | self.edit.SetLabel("Button clicked 2") 147 | await asyncio.sleep(0.5) 148 | self.edit.SetLabel("Working 2") 149 | await asyncio.sleep(1.2) 150 | self.edit.SetLabel("Completed 2") 151 | 152 | async def async_job(self): 153 | t0 = 0 154 | while True: 155 | t = time.time() 156 | print ("hello", t - t0) 157 | t0 = t 158 | await asyncio.sleep(0.2) 159 | 160 | def callback(self, event): 161 | print ("huhhiu") 162 | 163 | 164 | class WxAsyncAppCombinedThroughputTest(wx.Frame): 165 | """ Send the maxiumun number of events both through the asyncio event loop and through 166 | the wx Event loop, using both "loop.call_soon" and "wx.PostEvent", measure throughput and latency""" 167 | def __init__(self, loop=None): 168 | super(WxAsyncAppCombinedThroughputTest, self).__init__() 169 | self.loop = loop 170 | AsyncBind(EVT_TEST_EVENT, self.wx_loop_func, self) 171 | 172 | self.wx_latency_sum = 0 173 | self.aio_latency_sum = 0 174 | self.wx_events_pending = 1 # the first one is sent in constructor 175 | self.aio_events_pending = 1 # the first one is sent in constructor 176 | self.total_events_sent = 0 177 | self.total_to_send = 2000000 178 | self.total_wx_sent = 0 179 | self.total_aio_sent = 0 180 | self.tstart = time.time() 181 | wx.PostEvent(self, TestEvent(t1=self.tstart)) 182 | self.loop.call_soon(self.aio_loop_func, self.tstart) #, *args 183 | 184 | async def wx_loop_func(self, event): 185 | tnow = time.time() 186 | self.wx_latency_sum += (tnow - event.t1) 187 | self.wx_events_pending -= 1 188 | 189 | if self.total_events_sent == self.total_to_send and self.aio_events_pending == 0 and self.wx_events_pending == 0: 190 | self.tend = time.time() 191 | self.duration = self.tend-self.tstart 192 | self.Destroy() 193 | 194 | if self.total_events_sent != self.total_to_send: 195 | while self.wx_events_pending < 1000: 196 | wx.PostEvent(self, TestEvent(t1=tnow)) 197 | self.total_events_sent += 1 198 | self.total_wx_sent += 1 199 | self.wx_events_pending += 1 200 | 201 | def aio_loop_func(self, t1): 202 | tnow = time.time() 203 | self.aio_latency_sum += (tnow - t1) 204 | self.aio_events_pending -= 1 205 | 206 | if self.total_events_sent == self.total_to_send and self.aio_events_pending == 0 and self.wx_events_pending == 0: 207 | self.tend = time.time() 208 | self.duration = self.tend - self.tstart 209 | self.Destroy() 210 | 211 | if self.total_events_sent != self.total_to_send: 212 | while self.aio_events_pending < 1000: 213 | self.loop.call_soon(self.aio_loop_func, tnow) #, *args 214 | self.total_events_sent += 1 215 | self.aio_events_pending += 1 216 | self.total_aio_sent += 1 217 | 218 | def results(self): 219 | avg_aio_latency_ms = (self.aio_latency_sum / self.total_aio_sent) * 1000 220 | avg_wx_latency_ms = (self.wx_latency_sum / self.total_wx_sent) * 1000 221 | return { 222 | "wxThroughput(msg/s)": int(self.total_wx_sent/self.duration), 223 | "wxAvgLatency(ms)": int(avg_wx_latency_ms), 224 | "wxTotalSent": self.total_wx_sent, 225 | "aioThroughput(msg/s)": int(self.total_aio_sent/self.duration), 226 | "aioAvgLatency(ms)": int(avg_aio_latency_ms), 227 | "aioTotalSent": self.total_aio_sent, 228 | "Duration": "%.2fs" % (self.duration), 229 | "MsgInterval(ms)" : "none"} 230 | 231 | @staticmethod 232 | def run(): 233 | loop = get_event_loop() 234 | app = WxAsyncApp() 235 | frame = WxAsyncAppCombinedThroughputTest(loop=loop) 236 | loop.run_until_complete(app.MainLoop()) 237 | return frame.results() 238 | 239 | class WxMessageThroughputTest(wx.Frame): 240 | def __init__(self, parent=None): 241 | super(WxMessageThroughputTest, self).__init__(parent) 242 | self.latency_sum = 0 243 | self.wx_events_pending = 1 # the first one is sent in constructor 244 | self.total_events_sent = 0 245 | self.total_to_send = 100000 246 | self.tstart = time.time() 247 | self.Bind(EVT_TEST_EVENT, self.loop_func) 248 | wx.PostEvent(self, TestEvent(t1=time.time())) 249 | 250 | def loop_func(self, event): 251 | tnow = time.time() 252 | self.latency_sum += (tnow - event.t1) 253 | #self.wx_events_received.append(event.t1) 254 | self.wx_events_pending -= 1 255 | 256 | if self.total_events_sent == self.total_to_send and self.wx_events_pending == 0: 257 | self.tend = time.time() 258 | self.duration = self.tend - self.tstart 259 | self.Destroy() 260 | 261 | if self.total_events_sent != self.total_to_send: 262 | while self.wx_events_pending < 1000: 263 | wx.PostEvent(self, TestEvent(t1=tnow)) 264 | self.total_events_sent += 1 265 | self.wx_events_pending += 1 266 | 267 | def Destroy(self): 268 | super(WxMessageThroughputTest, self).Destroy() 269 | 270 | def results(self): 271 | avg_latency_ms = (self.latency_sum / self.total_to_send) * 1000 272 | return { 273 | "Throughput(msg/s)": int(self.total_to_send/self.duration), 274 | "AvgLatency(ms)": int(avg_latency_ms), 275 | "TotalSent": self.total_to_send, 276 | "Duration": "%.2fs" % (self.duration)} 277 | @staticmethod 278 | def run(): 279 | app = wx.App() 280 | frame = WxMessageThroughputTest() 281 | app.MainLoop() 282 | return frame.results() 283 | #print ("wx.App (wx.PostEvent)", frame.results()) 284 | 285 | QS_ALLINPUT = 0x04FF 286 | QS_ALLPOSTMESSAGE = 0x0100 287 | MWMO_ALERTABLE = 0x0002 288 | 289 | 290 | WAIT_TIMEOUT = 258 291 | WAIT_IO_COMPLETION = 0x000000C0 292 | 293 | if __name__ == '__main__': 294 | import _overlapped 295 | 296 | orig = _overlapped.GetQueuedCompletionStatus 297 | def GetQueuedCompletionStatus(iocp, ms): 298 | handles = ctypes.c_int(iocp) 299 | result = ctypes.windll.user32.MsgWaitForMultipleObjectsEx(1, ctypes.byref(handles), 1, QS_ALLINPUT|QS_ALLPOSTMESSAGE, MWMO_ALERTABLE) 300 | if result == WAIT_IO_COMPLETION: 301 | return orig (iocp, ms) 302 | elif result == WAIT_TIMEOUT: 303 | return 304 | app = wx.App.Get() 305 | if type(app) is not WxAsyncApp: 306 | raise Exception("Create a 'WxAsyncApp' first") 307 | app.DispatchMessages() 308 | 309 | _overlapped.GetQueuedCompletionStatus = GetQueuedCompletionStatus 310 | loop = asyncio.ProactorEventLoop() 311 | 312 | asyncio.set_event_loop(loop) 313 | 314 | 315 | app = WxAsyncApp() 316 | 317 | '''frame = TestFrame() 318 | frame.Show() 319 | app.SetTopWindow(frame) 320 | loop = get_event_loop() 321 | loop.run_until_complete(app.MainLoop()) 322 | ''' 323 | print (WxMessageThroughputTest.run()) -------------------------------------------------------------------------------- /test/test_perfs.py: -------------------------------------------------------------------------------- 1 | from wxasync import AsyncBind, WxAsyncApp 2 | import wx 3 | import asyncio 4 | from asyncio.events import get_event_loop 5 | from wx.lib.newevent import NewEvent 6 | import time 7 | from collections import deque 8 | import json 9 | from multiprocessing import Process 10 | import multiprocessing 11 | import itertools 12 | 13 | TestEvent, EVT_TEST_EVENT = NewEvent() 14 | 15 | class WxCallAfterThroughputTest(wx.Frame): 16 | def __init__(self, parent=None): 17 | super(WxCallAfterThroughputTest, self).__init__(parent) 18 | self.latency_sum = 0 19 | self.wx_events_pending = 1 # the first one is sent in constructor 20 | self.total_events_sent = 0 21 | self.total_to_send = 100000 22 | self.tstart = time.time() 23 | wx.CallAfter(self.loop_func, self.tstart) 24 | 25 | def loop_func(self, t1): 26 | tnow = time.time() 27 | self.latency_sum += (tnow - t1) 28 | #self.wx_events_received.append(event.t1) 29 | self.wx_events_pending -= 1 30 | 31 | if self.total_events_sent == self.total_to_send and self.wx_events_pending == 0: 32 | self.tend = time.time() 33 | self.duration = self.tend - self.tstart 34 | self.Destroy() 35 | 36 | if self.total_events_sent != self.total_to_send: 37 | while self.wx_events_pending < 1000: 38 | wx.CallAfter(self.loop_func, tnow) 39 | self.total_events_sent += 1 40 | self.wx_events_pending += 1 41 | 42 | def Destroy(self): 43 | super(WxCallAfterThroughputTest, self).Destroy() 44 | 45 | def results(self): 46 | avg_latency_ms = (self.latency_sum / self.total_to_send) * 1000 47 | return { 48 | "Throughput (msg/s)": int(self.total_to_send/self.duration), 49 | "Avg Latency (ms)": int(avg_latency_ms), 50 | "TotalSent": self.total_to_send, 51 | "Duration": "%.2fs" % (self.duration)} 52 | @staticmethod 53 | def run(): 54 | app = wx.App() 55 | frame = WxCallAfterThroughputTest() 56 | app.MainLoop() 57 | return (json.dumps(frame.results())) 58 | 59 | class WxMessageThroughputTest(wx.Frame): 60 | def __init__(self, parent=None): 61 | super(WxMessageThroughputTest, self).__init__(parent) 62 | self.latency_sum = 0 63 | self.wx_events_pending = 1 # the first one is sent in constructor 64 | self.total_events_sent = 0 65 | self.total_to_send = 100000 66 | self.tstart = time.time() 67 | self.Bind(EVT_TEST_EVENT, self.loop_func) 68 | wx.PostEvent(self, TestEvent(t1=time.time())) 69 | 70 | def loop_func(self, event): 71 | tnow = time.time() 72 | self.latency_sum += (tnow - event.t1) 73 | #self.wx_events_received.append(event.t1) 74 | self.wx_events_pending -= 1 75 | 76 | if self.total_events_sent == self.total_to_send and self.wx_events_pending == 0: 77 | self.tend = time.time() 78 | self.duration = self.tend - self.tstart 79 | self.Destroy() 80 | 81 | if self.total_events_sent != self.total_to_send: 82 | while self.wx_events_pending < 1000: 83 | wx.PostEvent(self, TestEvent(t1=tnow)) 84 | self.total_events_sent += 1 85 | self.wx_events_pending += 1 86 | 87 | def Destroy(self): 88 | super(WxMessageThroughputTest, self).Destroy() 89 | 90 | def results(self): 91 | avg_latency_ms = (self.latency_sum / self.total_to_send) * 1000 92 | return { 93 | "Throughput(msg/s)": int(self.total_to_send/self.duration), 94 | "AvgLatency(ms)": int(avg_latency_ms), 95 | "TotalSent": self.total_to_send, 96 | "Duration": "%.2fs" % (self.duration)} 97 | @staticmethod 98 | def run(): 99 | app = wx.App() 100 | frame = WxMessageThroughputTest() 101 | app.MainLoop() 102 | return frame.results() 103 | #print ("wx.App (wx.PostEvent)", frame.results()) 104 | 105 | 106 | class WxAsyncAsyncIOLatencyTest(wx.Frame): 107 | """ While using a WxAsyncApp, send only a few messages as 'loop.call_later' (not enough to queue up) through the asyncio event loop and check difference between expected time, and arrived time. 108 | If the GUI Dispatch blocks the asyncio event loop, you will see it here (). 109 | """ 110 | def __init__(self, parent=None, loop=None): 111 | super(WxAsyncAsyncIOLatencyTest, self).__init__(parent) 112 | self.loop = loop 113 | self.delay_ms = 100 114 | self.delay_s = self.delay_ms / 1000 115 | self.aio_latency_sum = 0 116 | self.aio_events_pending = 1 # the first one is sent in constructor 117 | self.total_events_sent = 0 118 | self.total_to_send = 100 119 | self.total_aio_sent = 0 120 | self.tstart = time.time() 121 | self.loop.call_soon(self.aio_loop_func, self.tstart) 122 | 123 | def aio_loop_func(self, t1): 124 | tnow = time.time() 125 | self.aio_latency_sum += (tnow - t1) 126 | self.aio_events_pending -= 1 127 | 128 | if self.total_events_sent == self.total_to_send and self.aio_events_pending == 0: 129 | self.tend = time.time() 130 | self.duration = self.tend - self.tstart 131 | self.Destroy() 132 | 133 | if self.total_events_sent != self.total_to_send: 134 | t = tnow + self.delay_s 135 | self.loop.call_later(self.delay_s, lambda: self.aio_loop_func(tnow+self.delay_s)) 136 | self.total_events_sent += 1 137 | self.aio_events_pending += 1 138 | self.total_aio_sent += 1 139 | 140 | def results(self): 141 | avg_aio_latency_ms = (self.aio_latency_sum / self.total_aio_sent) * 1000 142 | return { 143 | "AvgLatency(ms)": max(int(avg_aio_latency_ms), 0), 144 | "TotalSent": self.total_aio_sent, 145 | "Duration": "%.2fs" % (self.duration), 146 | "MsgInterval(ms)" : self.delay_ms} 147 | @staticmethod 148 | def run(): 149 | loop = get_event_loop() 150 | app = WxAsyncApp() 151 | frame = WxAsyncAsyncIOLatencyTest(loop=loop) 152 | loop.run_until_complete(app.MainLoop()) 153 | return frame.results() 154 | 155 | 156 | 157 | class WxAsyncAppMessageThroughputTest(wx.Frame): 158 | def __init__(self, parent=None): 159 | super(WxAsyncAppMessageThroughputTest, self).__init__(parent) 160 | vbox = wx.BoxSizer(wx.VERTICAL) 161 | button1 = wx.Button(self, label="Submit") 162 | vbox.Add(button1, 1, wx.EXPAND|wx.ALL) 163 | self.SetSizer(vbox) 164 | self.Layout() 165 | AsyncBind(EVT_TEST_EVENT, self.async_callback, self) 166 | wx.PostEvent(self, TestEvent(t1=time.time())) 167 | 168 | self.latency_sum = 0 169 | self.wx_events_pending = 1 # the first one is sent in constructor 170 | self.total_events_sent = 0 171 | self.total_to_send = 100000 172 | self.tstart = time.time() 173 | 174 | async def async_callback(self, event): 175 | tnow = time.time() 176 | self.latency_sum += (tnow - event.t1) 177 | #self.wx_events_received.append(event.t1) 178 | self.wx_events_pending -= 1 179 | 180 | if self.total_events_sent == self.total_to_send and self.wx_events_pending == 0: 181 | self.tend = time.time() 182 | self.duration = self.tend-self.tstart 183 | self.Destroy() 184 | 185 | if self.total_events_sent != self.total_to_send: 186 | while self.wx_events_pending < 1000: 187 | wx.PostEvent(self, TestEvent(t1=tnow)) 188 | self.total_events_sent += 1 189 | self.wx_events_pending += 1 190 | 191 | def Destroy(self): 192 | super(WxAsyncAppMessageThroughputTest, self).Destroy() 193 | 194 | def results(self): 195 | avg_latency_ms = (self.latency_sum / self.total_to_send) * 1000 196 | return { 197 | "Throughput(msg/s)": int(self.total_to_send/self.duration), 198 | "AvgLatency(ms)": int(avg_latency_ms), 199 | "TotalSent": self.total_to_send, 200 | "Duration": "%.2fs" % (self.duration)} 201 | @staticmethod 202 | def run(): 203 | app = WxAsyncApp() 204 | frame = WxAsyncAppMessageThroughputTest() 205 | loop = get_event_loop() 206 | loop.run_until_complete(app.MainLoop()) 207 | #app.__del__() 208 | return frame.results() 209 | 210 | 211 | class WxAsyncAppCombinedThroughputTest(wx.Frame): 212 | """ Send the maxiumun number of events both through the asyncio event loop and through 213 | the wx Event loop, using both "loop.call_soon" and "wx.PostEvent", measure throughput and latency""" 214 | def __init__(self, loop=None): 215 | super(WxAsyncAppCombinedThroughputTest, self).__init__() 216 | self.loop = loop 217 | AsyncBind(EVT_TEST_EVENT, self.wx_loop_func, self) 218 | 219 | self.wx_latency_sum = 0 220 | self.aio_latency_sum = 0 221 | self.wx_events_pending = 1 # the first one is sent in constructor 222 | self.aio_events_pending = 1 # the first one is sent in constructor 223 | self.total_events_sent = 0 224 | self.total_to_send = 200000 225 | self.total_wx_sent = 0 226 | self.total_aio_sent = 0 227 | self.tstart = time.time() 228 | wx.PostEvent(self, TestEvent(t1=self.tstart)) 229 | self.loop.call_soon(self.aio_loop_func, self.tstart) #, *args 230 | 231 | async def wx_loop_func(self, event): 232 | tnow = time.time() 233 | self.wx_latency_sum += (tnow - event.t1) 234 | self.wx_events_pending -= 1 235 | 236 | if self.total_events_sent == self.total_to_send and self.aio_events_pending == 0 and self.wx_events_pending == 0: 237 | self.tend = time.time() 238 | self.duration = self.tend-self.tstart 239 | self.Destroy() 240 | 241 | if self.total_events_sent != self.total_to_send: 242 | while self.wx_events_pending < 1000: 243 | wx.PostEvent(self, TestEvent(t1=tnow)) 244 | self.total_events_sent += 1 245 | self.total_wx_sent += 1 246 | self.wx_events_pending += 1 247 | 248 | def aio_loop_func(self, t1): 249 | tnow = time.time() 250 | self.aio_latency_sum += (tnow - t1) 251 | self.aio_events_pending -= 1 252 | 253 | if self.total_events_sent == self.total_to_send and self.aio_events_pending == 0 and self.wx_events_pending == 0: 254 | self.tend = time.time() 255 | self.duration = self.tend - self.tstart 256 | self.Destroy() 257 | 258 | if self.total_events_sent != self.total_to_send: 259 | while self.aio_events_pending < 1000: 260 | self.loop.call_soon(self.aio_loop_func, tnow) #, *args 261 | self.total_events_sent += 1 262 | self.aio_events_pending += 1 263 | self.total_aio_sent += 1 264 | 265 | def results(self): 266 | avg_aio_latency_ms = (self.aio_latency_sum / self.total_aio_sent) * 1000 267 | avg_wx_latency_ms = (self.wx_latency_sum / self.total_wx_sent) * 1000 268 | return { 269 | "wxThroughput(msg/s)": int(self.total_wx_sent/self.duration), 270 | "wxAvgLatency(ms)": int(avg_wx_latency_ms), 271 | "wxTotalSent": self.total_wx_sent, 272 | "aioThroughput(msg/s)": int(self.total_aio_sent/self.duration), 273 | "aioAvgLatency(ms)": int(avg_aio_latency_ms), 274 | "aioTotalSent": self.total_aio_sent, 275 | "Duration": "%.2fs" % (self.duration), 276 | "MsgInterval(ms)" : "none"} 277 | 278 | @staticmethod 279 | def run(): 280 | loop = get_event_loop() 281 | app = WxAsyncApp() 282 | frame = WxAsyncAppCombinedThroughputTest(loop=loop) 283 | loop.run_until_complete(app.MainLoop()) 284 | return frame.results() 285 | #print ("wxAsyncApp (wx.PostEvent+loop.call_soon)", frame.results()) 286 | 287 | class WxAsyncAppCombinedLatencyTest(wx.Frame): 288 | """ Send few events (not enough to queue up the event loop) both through the asyncio event loop and through 289 | the wx Event loop, using both "loop.call_soon" and "wx.PostEvent", and measure latency""" 290 | def __init__(self, parent=None, loop=None): 291 | super(WxAsyncAppCombinedLatencyTest, self).__init__(parent) 292 | self.loop = loop 293 | AsyncBind(EVT_TEST_EVENT, self.wx_loop_func, self) 294 | self.delay_ms = 10 295 | self.delay_s = self.delay_ms / 1000 296 | self.wx_latency_sum = 0 297 | self.aio_latency_sum = 0 298 | self.wx_events_pending = 1 # the first one is sent in constructor 299 | self.aio_events_pending = 1 # the first one is sent in constructor 300 | self.total_events_sent = 0 301 | self.total_to_send = 1000 302 | self.total_wx_sent = 0 303 | self.total_aio_sent = 0 304 | self.tstart = time.time() 305 | wx.PostEvent(self, TestEvent(t1=self.tstart)) 306 | self.loop.call_soon(self.aio_loop_func, self.tstart) #, *args 307 | 308 | async def wx_loop_func(self, event): 309 | tnow = time.time() 310 | self.wx_latency_sum += (tnow - event.t1) 311 | #print (tnow - event.t1, tnow, event.t1) 312 | #self.wx_events_received.append(event.t1) 313 | self.wx_events_pending -= 1 314 | 315 | if self.total_events_sent == self.total_to_send and self.wx_events_pending == 0: 316 | self.tend = time.time() 317 | self.duration = self.tend-self.tstart 318 | self.Destroy() 319 | 320 | if self.total_events_sent != self.total_to_send: 321 | wx.CallLater(self.delay_ms, lambda : wx.PostEvent(self, TestEvent(t1=tnow+self.delay_s))) 322 | self.total_events_sent += 1 323 | self.total_wx_sent += 1 324 | self.wx_events_pending += 1 325 | 326 | def aio_loop_func(self, t1): 327 | tnow = time.time() 328 | self.aio_latency_sum += (tnow - t1) 329 | self.aio_events_pending -= 1 330 | 331 | if self.total_events_sent == self.total_to_send and self.aio_events_pending == 0: 332 | self.tend = time.time() 333 | self.duration = self.tend - self.tstart 334 | self.Destroy() 335 | 336 | if self.total_events_sent != self.total_to_send: 337 | self.loop.call_later(self.delay_s, lambda: self.aio_loop_func(tnow+self.delay_s)) 338 | self.total_events_sent += 1 339 | self.aio_events_pending += 1 340 | self.total_aio_sent += 1 341 | 342 | def Destroy(self): 343 | super(WxAsyncAppCombinedLatencyTest, self).Destroy() 344 | 345 | def results(self): 346 | avg_aio_latency_ms = (self.aio_latency_sum / self.total_aio_sent) * 1000 347 | avg_wx_latency_ms = (self.wx_latency_sum / self.total_wx_sent) * 1000 348 | return { 349 | "wxAvgLatency(ms)": max(int(avg_wx_latency_ms), 0), # can be negative, due to "loop.call_later" calling a few ms earlier than expected 350 | "wxTotalSent": self.total_wx_sent, 351 | "aioAvgLatency(ms)": max(int(avg_aio_latency_ms), 0), # can be negative, due to "loop.call_later" calling a few ms earlier than expected 352 | "aioTotalSent": self.total_aio_sent, 353 | "Duration": "%.2fs" % (self.duration), 354 | "MsgInterval(ms)" : self.delay_ms} 355 | @staticmethod 356 | def run(): 357 | loop = get_event_loop() 358 | app = WxAsyncApp() 359 | frame = WxAsyncAppCombinedLatencyTest(loop=loop) 360 | loop.run_until_complete(app.MainLoop()) 361 | return frame.results() 362 | 363 | 364 | class AioThroughputTest(): 365 | """ Send the maxiumun number of events through the asyncio event loop using "loop.call_soon" and measure throughput and latency""" 366 | def __init__(self, loop): 367 | self.loop = loop 368 | self.latency_sum = 0 369 | self.events_pending = 1 # the first one is sent in constructor 370 | self.total_events_sent = 0 371 | self.total_to_send = 200000 372 | self.tstart = time.time() 373 | self.loop.call_soon(self.loop_func, self.tstart) 374 | 375 | def loop_func(self, t1): 376 | tnow = time.time() 377 | self.latency_sum += (tnow - t1) 378 | self.events_pending -= 1 379 | 380 | if self.total_events_sent == self.total_to_send and self.events_pending == 0: 381 | self.tend = time.time() 382 | self.duration = self.tend - self.tstart 383 | self.loop.stop() 384 | 385 | if self.total_events_sent != self.total_to_send: 386 | while self.events_pending < 10000: 387 | self.loop.call_soon(self.loop_func, tnow) #, *args 388 | self.total_events_sent += 1 389 | self.events_pending += 1 390 | 391 | def results(self): 392 | avg_latency_ms = (self.latency_sum / self.total_to_send) * 1000 393 | return { 394 | "Throughput(msg/s)": int(self.total_to_send/self.duration), 395 | "AvgLatency(ms)": int(avg_latency_ms), 396 | "TotalSent": self.total_to_send, 397 | "Duration": "%.2fs" % (self.duration)} 398 | @staticmethod 399 | def run(): 400 | loop = get_event_loop() 401 | test = AioThroughputTest(loop) 402 | loop.run_forever() 403 | return test.results() 404 | 405 | def call_and_queue_result(func, queue): 406 | queue.put(func()) 407 | 408 | def run_in_bg_process(func): 409 | queue = multiprocessing.Queue() 410 | p = Process(target=call_and_queue_result, args=(func, queue)) 411 | p.start() 412 | p.join() 413 | return queue.get() 414 | 415 | def flatten_list(lst_of_lst): 416 | return list(itertools.chain.from_iterable(lst_of_lst)) 417 | 418 | def format_table(data): 419 | lens = [[len(str(e)) for e in row] for row in data] 420 | collens = [max(col) for col in zip(*lens)] 421 | row_strs = [] 422 | for row in data: 423 | row_strs.append(" ".join([str(elm).rjust(l) if i else str(elm).ljust(l) for i, (l, elm) in enumerate(zip(collens, row))])) 424 | return "\n".join(row_strs) 425 | 426 | def format_stat_results(results): 427 | columns = sorted(set(flatten_list([statlist.keys() for statlist in results.values()]))) 428 | table = [[""] + columns] 429 | for key, stats in results.items(): 430 | table.append([key] + [stats.get(c, "") for c in columns]) 431 | return (format_table(table)) 432 | 433 | if __name__ == '__main__': 434 | results = {} 435 | results = {} 436 | results["wxAsyncApp (asyncio latency)"] = WxAsyncAsyncIOLatencyTest.run() 437 | 438 | results["wx.App (wx.PostEvent)"] = WxMessageThroughputTest.run() 439 | results["asyncio (loop.call_soon)"] = AioThroughputTest.run() 440 | results["wxAsyncApp (wx.PostEvent)"] = WxAsyncAppMessageThroughputTest.run() 441 | 442 | print ("Individual Tests: ") 443 | print (format_stat_results(results)) 444 | 445 | combined_results = {} 446 | combined_results["wxAsyncApp (wx.PostEvent+loop.call_soon) throughput"] = WxAsyncAppCombinedThroughputTest.run() 447 | combined_results["wxAsyncApp (wx.PostEvent+loop.call_soon) latency"] = WxAsyncAppCombinedLatencyTest.run() 448 | print ("\nCombined Tests, using wx and asyncio at the same time:\n") 449 | print (format_stat_results(combined_results)) 450 | 451 | 452 | """ 453 | Windows (Core I7-7700K 4.2Ghz): 454 | 455 | Individual Tests: 456 | AvgLatency(ms) Duration MsgInterval(ms) Throughput(msg/s) TotalSent 457 | wxAsyncApp (asyncio latency) 0 9.37s 100 100 458 | wx.App (wx.PostEvent) 11 1.13s 88561 100000 459 | asyncio (loop.call_soon) 17 0.35s 571325 200000 460 | wxAsyncApp (wx.PostEvent) 19 1.99s 50279 100000 461 | 462 | Combined Tests, using wx and asyncio at the same time: 463 | 464 | Duration MsgInterval(ms) aioAvgLatency(ms) aioThroughput(msg/s) aioTotalSent wxAvgLatency(ms) wxThroughput(msg/s) wxTotalSent 465 | wxAsyncApp (wx.PostEvent+loop.call_soon) throughput 1.79s none 13 74763 134000 27 36823 66000 466 | wxAsyncApp (wx.PostEvent+loop.call_soon) latency 1.84s 10 -7 882 0 118 467 | 468 | On MacOS (VM of Core I7-7700K 4.2Ghz): 469 | 470 | Individual Tests: 471 | AvgLatency(ms) Duration MsgInterval(ms) Throughput(msg/s) TotalSent 472 | wxAsyncApp (asyncio latency) 0 10.08s 100 100 473 | wx.App (wx.PostEvent) 16 1.62s 61677 100000 474 | asyncio (loop.call_soon) 29 0.60s 332176 200000 475 | wxAsyncApp (wx.PostEvent) 25 2.55s 39244 100000 476 | 477 | Combined Tests, using wx and asyncio at the same time: 478 | 479 | Duration MsgInterval(ms) aioAvgLatency(ms) aioThroughput(msg/s) aioTotalSent wxAvgLatency(ms) wxThroughput(msg/s) wxTotalSent 480 | wxAsyncApp (wx.PostEvent+loop.call_soon) throughput 1.69s none 11 88970 150000 33 29656 50000 481 | wxAsyncApp (wx.PostEvent+loop.call_soon) latency 6.87s 10 0 629 8 371 482 | 483 | """ --------------------------------------------------------------------------------