Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable deep hashing if memoization is off #2007

Merged
merged 9 commits into from
Oct 23, 2017
20 changes: 12 additions & 8 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ def __init__(self, callable, **params):
self._is_overlay = False
self.args = None
self.kwargs = None
self._stream_memoization = self.memoize

@property
def argspec(self):
Expand All @@ -486,19 +487,20 @@ def clone(self, callable=None, **overrides):

def __call__(self, *args, **kwargs):
# Nothing to do for callbacks that accept no arguments
kwarg_hash = kwargs.pop('memoization_hash', [])
(self.args, self.kwargs) = (args, kwargs)
if not args and not kwargs: return self.callable()
inputs = [i for i in self.inputs if isinstance(i, DynamicMap)]
streams = []
for stream in [s for i in inputs for s in get_nested_streams(i)]:
if stream not in streams: streams.append(stream)

memoize = self.memoize and not any(s.transient and s._triggering for s in streams)
values = tuple(tuple(sorted(s.contents.items())) for s in streams)
key = args + tuple(sorted(kwargs.items())) + values
memoize = self._stream_memoization and not any(s.transient and s._triggering for s in streams)
values = tuple(tuple(sorted(s.hashkey.items())) for s in streams)
key = args + tuple(sorted(kwarg_hash)) + values

hashed_key = util.deephash(key)
if memoize and hashed_key in self._memoized:
hashed_key = util.deephash(key) if self.memoize else None
if hashed_key is not None and memoize and hashed_key in self._memoized:
return self._memoized[hashed_key]

if self.argspec.varargs is not None:
Expand Down Expand Up @@ -591,14 +593,14 @@ def dynamicmap_memoization(callable_obj, streams):
DynamicMap). Memoization is disabled if any of the streams require
it it and are currently in a triggered state.
"""
memoization_state = bool(callable_obj.memoize)
callable_obj.memoize &= not any(s.transient and s._triggering for s in streams)
memoization_state = bool(callable_obj._stream_memoization)
callable_obj._stream_memoization &= not any(s.transient and s._triggering for s in streams)
try:
yield
except:
raise
finally:
callable_obj.memoize = memoization_state
callable_obj._stream_memoization = memoization_state



Expand Down Expand Up @@ -837,6 +839,7 @@ def _execute_callback(self, *args):
# Additional validation needed to ensure kwargs don't clash
kdims = [kdim.name for kdim in self.kdims]
kwarg_items = [s.contents.items() for s in self.streams]
hash_items = tuple(tuple(sorted(s.hashkey.items())) for s in self.streams)
flattened = [(k,v) for kws in kwarg_items for (k,v) in kws
if k not in kdims]

Expand All @@ -845,6 +848,7 @@ def _execute_callback(self, *args):
args = ()
else:
kwargs = dict(flattened)
kwargs['memoization_hash'] = hash_items

with dynamicmap_memoization(self.callback, self.streams):
retval = self.callback(*args, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def default(self, obj):
elif isinstance(obj, np.ndarray):
return obj.tolist()
if pd and isinstance(obj, (pd.Series, pd.DataFrame)):
return repr(sorted(list(obj.to_dict().items())))
return obj.to_csv().encode('utf-8')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A fair bit faster, although still not great, hence also adding a hashkey.

elif isinstance(obj, self.string_hashable):
return str(obj)
elif isinstance(obj, self.repr_hashable):
Expand Down
43 changes: 41 additions & 2 deletions holoviews/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
server-side or in Javascript in the Jupyter notebook (client-side).
"""

import uuid

import param
import numpy as np
from numbers import Number
Expand Down Expand Up @@ -296,6 +298,15 @@ def contents(self):
return {self._rename.get(k,k):v for (k,v) in filtered.items()
if (self._rename.get(k,True) is not None)}

@property
def hashkey(self):
"""
The object the memoization hash is computed from. By default
returns the stream contents but can be overridden to provide
a custom hash key.
"""
return self.contents


def _set_stream_parameters(self, **kwargs):
"""
Expand Down Expand Up @@ -352,6 +363,34 @@ def transform(self):
return {'counter': self.counter + 1}


class StreamData(Stream):
"""
A Stream used to pipe arbitrary data to a callback.
Unlike other streams memoization can be disabled for a
StreamData stream (and is disabled by default).
"""

data = param.Parameter(default=None, constant=True, doc="""
Arbitrary data being streamed to a DynamicMap callback.""")

def __init__(self, memoize=False, **params):
super(StreamData, self).__init__(**params)
self._memoize = memoize

def send(self, data):
"""
A convenience method to send an event with data without
supplying a keyword.
"""
self.event(data=data)

@property
def hashkey(self):
if self._memoize:
return self.contents
return {'hash': uuid.uuid4().hex}


class LinkedStream(Stream):
"""
A LinkedStream indicates is automatically linked to plot interactions
Expand Down Expand Up @@ -404,11 +443,11 @@ class PointerXY(LinkedStream):
the plot bounds, the position values are set to None.
"""

x = param.ClassSelector(class_=(Number, util.basestring), default=None,
x = param.ClassSelector(class_=(Number, util.basestring, tuple), default=None,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated change that snuck in. Allows the Pointer stream to work with nested categorical axes so worth keeping (and I can't be bothered to make a new PR).

constant=True, doc="""
Pointer position along the x-axis in data coordinates""")

y = param.ClassSelector(class_=(Number, util.basestring), default=None,
y = param.ClassSelector(class_=(Number, util.basestring, tuple), default=None,
constant=True, doc="""
Pointer position along the y-axis in data coordinates""")

Expand Down
30 changes: 30 additions & 0 deletions tests/testdynamic.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from collections import deque
import time

Expand Down Expand Up @@ -572,6 +573,35 @@ def history_callback(x, y, history=deque(maxlen=10)):
self.assertEqual(xresets, 2)
self.assertEqual(yresets, 2)

def test_dynamic_callable_stream_hashkey(self):
# Enable transient stream meaning memoization only happens when
# stream is inactive, should have sample for each call to
# stream.update
def history_callback(x, history=deque(maxlen=10)):
if x is not None:
history.append(x)
return Curve(list(history))

class NoMemoize(PointerX):
@property
def hashkey(self): return {'hash': uuid.uuid4().hex}

x = NoMemoize()
dmap = DynamicMap(history_callback, kdims=[], streams=[x])

# Add stream subscriber mocking plot
x.add_subscriber(lambda **kwargs: dmap[()])

for i in range(2):
x.event(x=1)
self.assertEqual(dmap[()], Curve([1, 1, 1]))

for i in range(2):
x.event(x=2)

self.assertEqual(dmap[()], Curve([1, 1, 1, 2, 2, 2]))



class TestPeriodicStreamUpdate(ComparisonTestCase):

Expand Down