From 9f3a316ccf5ce05651da48765c4d08504f95cbf0 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 9 Dec 2018 13:31:51 -0500 Subject: [PATCH] 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