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).
kivy_mainline_and_py3.8
Tyler Goodlet 2018-12-09 13:31:51 -05:00
parent b8815cde4a
commit 9f3a316ccf
1 changed files with 77 additions and 7 deletions

View File

@ -3,10 +3,69 @@
Based on initial LGPL work by O. Poyen. here: Based on initial LGPL work by O. Poyen. here:
https://gist.github.com/opqopq/15c707dc4cffc2b6455f https://gist.github.com/opqopq/15c707dc4cffc2b6455f
""" """
import time
from functools import wraps
from collections import deque
from kivy.properties import BooleanProperty, ObjectProperty from kivy.properties import BooleanProperty, ObjectProperty
from kivy.core.window import Window 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): 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 # be used in `on_enter` or `on_leave` in order to know where was dispatched
# the event. # the event.
border_point = ObjectProperty(None) border_point = ObjectProperty(None)
_widgets = [] _widgets = deque()
_last_hovered = None _last_hovered = None
_last_time = time.time()
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.register_event_type('on_enter') self.register_event_type('on_enter')
self.register_event_type('on_leave') self.register_event_type('on_leave')
MouseOverBehavior._widgets.append(self) MouseOverBehavior._widgets.append(self)
super(MouseOverBehavior, self).__init__(**kwargs) super().__init__(**kwargs)
Window.bind(mouse_pos=self.on_mouse_pos) Window.bind(mouse_pos=self._on_mouse_pos)
@classmethod @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? # XXX: how to still do this at the class level?
# don't proceed if I'm not displayed <=> If have no parent # don't proceed if I'm not displayed <=> If have no parent
# if not self.get_root_window(): # if not self.get_root_window():
@ -42,7 +107,7 @@ class MouseOverBehavior(object):
pos = args[1] pos = args[1]
# Next line to_widget allow to compensate for relative layout # 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) w_coords = widget.to_widget(*pos)
inside = widget.collide_point(*w_coords) inside = widget.collide_point(*w_coords)
if inside and widget.hovered: if inside and widget.hovered:
@ -54,11 +119,16 @@ class MouseOverBehavior(object):
last_hovered.dispatch('on_leave') last_hovered.dispatch('on_leave')
last_hovered.hovered = False 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 # highlight new widget
widget.border_point = pos widget.border_point = pos
widget.hovered = True widget.hovered = True
widget.dispatch('on_enter') widget.dispatch('on_enter')
cls._last_hovered = widget
return return
# implement these in the widget impl # implement these in the widget impl