From 5c070b1c434e7e31fbb822289e2d984ebf137a98 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 1 Dec 2018 18:39:01 -0500 Subject: [PATCH 1/5] Faster highlighting via single loop and callback Thanks yet again to @tshirtman for suggesting this. Instead of defining a `on_mouse_pos()` on every widget simply register and track each widget and loop through them all once (or as much as is necessary) in a single callback. The assumption here is that we get a performance boost by looping widgets instead of having `kivy` loop and call back each widget thus avoiding costly python function calls. --- piker/ui/kivy/hoverable.py | 53 +++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/piker/ui/kivy/hoverable.py b/piker/ui/kivy/hoverable.py index 4aa70c24..fda4452f 100644 --- a/piker/ui/kivy/hoverable.py +++ b/piker/ui/kivy/hoverable.py @@ -5,7 +5,6 @@ __author__ = 'Olivier Poyen' from kivy.properties import BooleanProperty, ObjectProperty -from kivy.factory import Factory from kivy.core.window import Window @@ -23,38 +22,50 @@ class HoverBehavior(object): # be used in `on_enter` or `on_leave` in order to know where was dispatched # the event. border_point = ObjectProperty(None) + _widgets = [] + _last_hovered = None def __init__(self, **kwargs): self.register_event_type('on_enter') self.register_event_type('on_leave') - Window.bind(mouse_pos=self.on_mouse_pos) + HoverBehavior._widgets.append(self) super(HoverBehavior, self).__init__(**kwargs) + Window.bind(mouse_pos=self.on_mouse_pos) - def on_mouse_pos(self, *args): + @classmethod + def on_mouse_pos(cls, *args): + # XXX: how to still do this at the class level? # don't proceed if I'm not displayed <=> If have no parent - if not self.get_root_window(): - return + # if not self.get_root_window(): + # return + pos = args[1] # Next line to_widget allow to compensate for relative layout - inside = self.collide_point(*self.to_widget(*pos)) - if self.hovered == inside: - # We have already done what was needed - return - self.border_point = pos - self.hovered = inside - if inside: - self.dispatch('on_enter') - else: - self.dispatch('on_leave') + for widget in cls._widgets: + w_coords = widget.to_widget(*pos) + inside = widget.collide_point(*w_coords) + if inside and widget.hovered: + return + elif inside: + # un-highlight the last highlighted + last_hovered = cls._last_hovered + if last_hovered: + last_hovered.dispatch('on_leave') + last_hovered.hovered = False + + # highlight new widget + widget.border_point = pos + widget.hovered = True + widget.dispatch('on_enter') + cls._last_hovered = widget + return # implement these in the widget impl - def on_enter(self): + @classmethod + def on_enter(cls): pass - def on_leave(self): + @classmethod + def on_leave(cls): pass - - -# register for global use via kivy.factory.Factory -Factory.register('HoverBehavior', HoverBehavior) From fd94a24d84655cc1999a4c7f30bf8668fb7501e1 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 1 Dec 2018 18:43:44 -0500 Subject: [PATCH 2/5] Rename to `mouse_over` --- piker/ui/kivy/{hoverable.py => mouse_over.py} | 0 piker/ui/monitor.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename piker/ui/kivy/{hoverable.py => mouse_over.py} (100%) diff --git a/piker/ui/kivy/hoverable.py b/piker/ui/kivy/mouse_over.py similarity index 100% rename from piker/ui/kivy/hoverable.py rename to piker/ui/kivy/mouse_over.py diff --git a/piker/ui/monitor.py b/piker/ui/monitor.py index 44480a61..ed478aa9 100644 --- a/piker/ui/monitor.py +++ b/piker/ui/monitor.py @@ -23,7 +23,7 @@ from async_generator import aclosing from ..log import get_logger from .pager import PagerView -from .kivy.hoverable import HoverBehavior +from .kivy.mouse_over import HoverBehavior log = get_logger('monitor') From eee19048f0f58dd9a91a663c5893f9354beea9ab Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 1 Dec 2018 19:01:36 -0500 Subject: [PATCH 3/5] Support "mouse over" groups Add a type factory func which returns mixin-able types for creating mutex highlight-able groups of widgets. --- piker/ui/kivy/mouse_over.py | 27 ++++++++++++++++++++------- piker/ui/monitor.py | 4 +++- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/piker/ui/kivy/mouse_over.py b/piker/ui/kivy/mouse_over.py index fda4452f..88d0b108 100644 --- a/piker/ui/kivy/mouse_over.py +++ b/piker/ui/kivy/mouse_over.py @@ -1,15 +1,16 @@ -"""Hoverable Behaviour (changing when the mouse is on the widget by O. Poyen. -License: LGPL +"""Mouse over behaviour. + +Based on initial LGPL work by O. Poyen. here: +https://gist.github.com/opqopq/15c707dc4cffc2b6455f """ -__author__ = 'Olivier Poyen' from kivy.properties import BooleanProperty, ObjectProperty from kivy.core.window import Window -class HoverBehavior(object): - """Hover behavior. +class MouseOverBehavior(object): + """Mouse over behavior. :Events: `on_enter` @@ -28,8 +29,8 @@ class HoverBehavior(object): def __init__(self, **kwargs): self.register_event_type('on_enter') self.register_event_type('on_leave') - HoverBehavior._widgets.append(self) - super(HoverBehavior, self).__init__(**kwargs) + MouseOverBehavior._widgets.append(self) + super(MouseOverBehavior, self).__init__(**kwargs) Window.bind(mouse_pos=self.on_mouse_pos) @classmethod @@ -69,3 +70,15 @@ class HoverBehavior(object): @classmethod def on_leave(cls): pass + + +def new_mouse_over_group(): + """Return a new *mouse over group*, a class that can be mixed + in to a group of widgets which can be mutex highlighted based + on the mouse position. + """ + return type( + 'MouseOverBehavior', + (MouseOverBehavior,), + {}, + ) diff --git a/piker/ui/monitor.py b/piker/ui/monitor.py index ed478aa9..d9568135 100644 --- a/piker/ui/monitor.py +++ b/piker/ui/monitor.py @@ -23,8 +23,10 @@ from async_generator import aclosing from ..log import get_logger from .pager import PagerView -from .kivy.mouse_over import HoverBehavior +from .kivy.mouse_over import new_mouse_over_group + +HoverBehavior = new_mouse_over_group() log = get_logger('monitor') From b8815cde4a5844816d6f51862cf1e534828c1d2a Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 9 Dec 2018 13:30:34 -0500 Subject: [PATCH 4/5] Set statespace defaults in `get_cached_feed()` --- piker/brokers/data.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/piker/brokers/data.py b/piker/brokers/data.py index 56e99fac..e35b0dec 100644 --- a/piker/brokers/data.py +++ b/piker/brokers/data.py @@ -269,9 +269,10 @@ async def get_cached_feed( """ # check if a cached client is in the local actor's statespace ss = tractor.current_actor().statespace - feeds = ss['feeds'] + feeds = ss.setdefault('feeds', {'_lock': trio.Lock()}) lock = feeds['_lock'] - feed_stack = ss['feed_stacks'][brokername] + feed_stacks = ss.setdefault('feed_stacks', {}) + feed_stack = feed_stacks.setdefault(brokername, contextlib.AsyncExitStack()) async with lock: try: feed = feeds[brokername] @@ -305,18 +306,15 @@ async def start_quote_stream( Since most brokers seems to support batch quote requests we limit to one task per process for now. """ + actor = tractor.current_actor() # set log level after fork get_console_log(actor.loglevel) # pull global vars from local actor ss = actor.statespace - # broker2symbolsubs = ss.setdefault('broker2symbolsubs', {}) - ss.setdefault('feeds', {'_lock': trio.Lock()}) - feed_stacks = ss.setdefault('feed_stacks', {}) symbols = list(symbols) log.info( f"{chan.uid} subscribed to {broker} for symbols {symbols}") - feed_stack = feed_stacks.setdefault(broker, contextlib.AsyncExitStack()) # another actor task may have already created it feed = await get_cached_feed(broker) symbols2chans = feed.subscriptions[feed_type] From 9f3a316ccf5ce05651da48765c4d08504f95cbf0 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 9 Dec 2018 13:31:51 -0500 Subject: [PATCH 5/5] Improve CPU usage using a clock trigger and deque Copy out `kivy.clock.triggered` from version 1.10.1 since it isn't yet available in the `trio`/async branch and use it to throttle the callback rate. Use a `collections.deque` to LIFO iterate widgets each call using the heuristic that it's more likely the mouse is still within the currently highlighted (or it's adjacent neighbors) widget as opposed to some far away widget (the case when the mouse is moved very drastically across the window). --- piker/ui/kivy/mouse_over.py | 84 +++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/piker/ui/kivy/mouse_over.py b/piker/ui/kivy/mouse_over.py index 88d0b108..80a67d63 100644 --- a/piker/ui/kivy/mouse_over.py +++ b/piker/ui/kivy/mouse_over.py @@ -3,10 +3,69 @@ Based on initial LGPL work by O. Poyen. here: https://gist.github.com/opqopq/15c707dc4cffc2b6455f """ - +import time +from functools import wraps +from collections import deque from kivy.properties import BooleanProperty, ObjectProperty from kivy.core.window import Window +from kivy.clock import Clock + + +from ...log import get_logger + + +log = get_logger('kivy') + + +# XXX: copied from 1.10.1 since the async branch isn't ported yet. +def triggered(timeout=0, interval=False): + '''Decorator that will trigger the call of the function at the specified + timeout, through the method :meth:`CyClockBase.create_trigger`. Subsequent + calls to the decorated function (while the timeout is active) are ignored. + It can be helpful when an expensive funcion (i.e. call to a server) can be + triggered by different methods. Setting a proper timeout will delay the + calling and only one of them wil be triggered. + @triggered(timeout, interval=False) + def callback(id): + print('The callback has been called with id=%d' % id) + >> callback(id=1) + >> callback(id=2) + The callback has been called with id=2 + The decorated callback can also be unscheduled using: + >> callback.cancel() + .. versionadded:: 1.10.1 + ''' + + def wrapper_triggered(func): + + _args = [] + _kwargs = {} + + def cb_function(dt): + func(*tuple(_args), **_kwargs) + + cb_trigger = Clock.create_trigger( + cb_function, + timeout=timeout, + interval=interval) + + @wraps(func) + def trigger_function(*args, **kwargs): + _args[:] = [] + _args.extend(list(args)) + _kwargs.clear() + _kwargs.update(kwargs) + cb_trigger() + + def trigger_cancel(): + cb_trigger.cancel() + + setattr(trigger_function, 'cancel', trigger_cancel) + + return trigger_function + + return wrapper_triggered class MouseOverBehavior(object): @@ -23,18 +82,24 @@ class MouseOverBehavior(object): # be used in `on_enter` or `on_leave` in order to know where was dispatched # the event. border_point = ObjectProperty(None) - _widgets = [] + _widgets = deque() _last_hovered = None + _last_time = time.time() def __init__(self, **kwargs): self.register_event_type('on_enter') self.register_event_type('on_leave') MouseOverBehavior._widgets.append(self) - super(MouseOverBehavior, self).__init__(**kwargs) - Window.bind(mouse_pos=self.on_mouse_pos) + super().__init__(**kwargs) + Window.bind(mouse_pos=self._on_mouse_pos) @classmethod - def on_mouse_pos(cls, *args): + # try throttling to 1ms latency (doesn't seem to work + # best I can get is 0.01...) + @triggered(timeout=0.001, interval=False) + def _on_mouse_pos(cls, *args): + log.debug(f"{cls} time since last call: {time.time() - cls._last_time}") + cls._last_time = time.time() # XXX: how to still do this at the class level? # don't proceed if I'm not displayed <=> If have no parent # if not self.get_root_window(): @@ -42,7 +107,7 @@ class MouseOverBehavior(object): pos = args[1] # Next line to_widget allow to compensate for relative layout - for widget in cls._widgets: + for widget in cls._widgets.copy(): w_coords = widget.to_widget(*pos) inside = widget.collide_point(*w_coords) if inside and widget.hovered: @@ -54,11 +119,16 @@ class MouseOverBehavior(object): last_hovered.dispatch('on_leave') last_hovered.hovered = False + # stick last highlighted at the front of the stack + # resulting in LIFO iteration for efficiency + cls._widgets.remove(widget) + cls._widgets.appendleft(widget) + cls._last_hovered = widget + # highlight new widget widget.border_point = pos widget.hovered = True widget.dispatch('on_enter') - cls._last_hovered = widget return # implement these in the widget impl