Repository: vispy/vispy

Issue:
A recent commit caused a big framerate drop in 3d
The commit (found by bisection) is 6e82de1b3408555f6ce23b45b4568b66e2e86eb4. I'm not sure if it only affects 3d scenes, but that's the only place I've directly experienced it. After asking on gitter about it, I checked if it's fixed by #873, but it seems it isn't.

I first noticed this in one of my own scenes, with a small number of visuals, that previously could be updated at 60fps with no issues but following this commit takes well over a second to update under mouse interaction.

It's also detectable (but less obvious) in e.g. the examples/basics/visuals/tube.py example, though the framerate drop is not too large.

This can be demonstrated by directly reverting 6e82de1b3408555f6ce23b45b4568b66e2e86eb4 , after which the problem is resolved (others are reintroduced, including the breaking of depth-testing, but it demonstrates that this commit is where the issue directly appears).



Discussion:
The problematic line seems to be 243 in vispy/visuals/shaders/function.py,  though the real issue could be something elsewhere listening to the events and doing too much in response, I'm not sure what is intended here.


Resolving patch:
diff --git a/vispy/visuals/shaders/function.py b/vispy/visuals/shaders/function.py
--- a/vispy/visuals/shaders/function.py
+++ b/vispy/visuals/shaders/function.py
@@ -19,6 +19,7 @@
 
 import re
 import logging
+import numpy as np
 
 from ...util.eq import eq
 from ...util import logger
@@ -238,14 +239,10 @@ def __setitem__(self, key, val):
             # try just updating its value.
             variable = storage.get(key, None)
             if isinstance(variable, Variable):
-                try:
+                if np.any(variable.value != val):
                     variable.value = val
                     self.changed(value_changed=True)
-                    return
-                except Exception:
-                    # Setting value on existing Variable failed for some
-                    # reason; will need to create a new Variable instead. 
-                    pass
+                return
             
             # Could not set variable.value directly; instead we will need
             # to create a new ShaderObject
@@ -465,7 +462,7 @@ def _get_replaced_code(self, names):
         if '$' in code:
             v = parsing.find_template_variables(code)
             logger.warning('Unsubstituted placeholders in code: %s\n'
-                           '  replacements made: %s' % 
+                           '  replacements made: %s', 
                            (v, list(self._expressions.keys())))
         
         return code + '\n'
diff --git a/vispy/visuals/shaders/program.py b/vispy/visuals/shaders/program.py
--- a/vispy/visuals/shaders/program.py
+++ b/vispy/visuals/shaders/program.py
@@ -38,7 +38,7 @@ def vert(self):
     @vert.setter
     def vert(self, vcode):
         if hasattr(self, '_vert') and self._vert is not None:
-            self._vert.changed.disconnect((self, '_source_changed'))
+            self._vert._dependents.pop(self)
 
         self._vert = vcode
         if self._vert is None:
@@ -46,7 +46,7 @@ def vert(self, vcode):
 
         vcode = preprocess(vcode)
         self._vert = MainFunction(vcode)
-        self._vert.changed.connect((self, '_source_changed'))
+        self._vert._dependents[self] = None
 
         self._need_build = True
         self.changed(code_changed=True, value_changed=False)
@@ -58,7 +58,7 @@ def frag(self):
     @frag.setter
     def frag(self, fcode):
         if hasattr(self, '_frag') and self._frag is not None:
-            self._frag.changed.disconnect((self, '_source_changed'))
+            self._frag._dependents.pop(self)
 
         self._frag = fcode
         if self._frag is None:
@@ -66,7 +66,7 @@ def frag(self, fcode):
 
         fcode = preprocess(fcode)
         self._frag = MainFunction(fcode)
-        self._frag.changed.connect((self, '_source_changed'))
+        self._frag._dependents[self] = None
 
         self._need_build = True
         self.changed(code_changed=True, value_changed=False)
@@ -77,12 +77,12 @@ def prepare(self):
         pass
         # todo: remove!
 
-    def _source_changed(self, ev):
+    def _dep_changed(self, dep, code_changed=False, value_changed=False):
         logger.debug("ModularProgram source changed: %s", self)
-        if ev.code_changed:
+        if code_changed:
             self._need_build = True
-        self.changed(code_changed=ev.code_changed, 
-                     value_changed=ev.value_changed)
+        self.changed(code_changed=code_changed, 
+                     value_changed=value_changed)
     
     def draw(self, *args, **kwargs):
         self.build_if_needed()
diff --git a/vispy/visuals/shaders/shader_object.py b/vispy/visuals/shaders/shader_object.py
--- a/vispy/visuals/shaders/shader_object.py
+++ b/vispy/visuals/shaders/shader_object.py
@@ -1,20 +1,14 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2014, Vispy Development Team.
 # Distributed under the (new) BSD License. See LICENSE.txt for more info.
+from weakref import WeakKeyDictionary
+
 from ...util import logger
-from ...util.event import EventEmitter, Event
 from ...ext.ordereddict import OrderedDict
 from ...ext.six import string_types
 from .compiler import Compiler
 
 
-class ShaderChangeEvent(Event):
-    def __init__(self, code_changed=False, value_changed=False, **kwargs):
-        Event.__init__(self, type='shader_change', **kwargs)
-        self.code_changed = code_changed
-        self.value_changed = value_changed
-
-
 class ShaderObject(object):
     """ Base class for all objects that may be included in a GLSL program
     (Functions, Variables, Expressions).
@@ -26,7 +20,7 @@ class ShaderObject(object):
     Dependencies are tracked hierarchically such that changes to any object
     will be propagated up the dependency hierarchy to trigger a recompile.
     """
-    
+
     @classmethod
     def create(self, obj, ref=None):
         """ Convert *obj* to a new ShaderObject. If the output is a Variable
@@ -57,13 +51,12 @@ def create(self, obj, ref=None):
         return obj
     
     def __init__(self):
-        # emitted when any part of the code for this object has changed,
-        # including dependencies.
-        self.changed = EventEmitter(source=self, event_class=ShaderChangeEvent)
-        
         # objects that must be declared before this object's definition.
         # {obj: refcount}
         self._deps = OrderedDict()  # OrderedDict for consistent code output
+        
+        # Objects that depend on this one will be informed of changes.
+        self._dependents = WeakKeyDictionary()
     
     @property
     def name(self):
@@ -121,7 +114,7 @@ def _add_dep(self, dep):
             self._deps[dep] += 1
         else:
             self._deps[dep] = 1
-            dep.changed.connect(self._dep_changed)
+            dep._dependents[self] = None
 
     def _remove_dep(self, dep):
         """ Decrement the reference count for *dep*. If the reference count 
@@ -131,15 +124,23 @@ def _remove_dep(self, dep):
         refcount = self._deps[dep]
         if refcount == 1:
             self._deps.pop(dep)
-            dep.changed.disconnect(self._dep_changed)
+            dep._dependents.pop(self)
         else:
             self._deps[dep] -= 1
-        
-    def _dep_changed(self, event):
+
+    def _dep_changed(self, dep, code_changed=False, value_changed=False):
         """ Called when a dependency's expression has changed.
         """
-        logger.debug("ShaderObject changed: %r" % event.source)
-        self.changed(event)
+        logger.debug("ShaderObject changed [code=%s, value=%s]", code_changed,
+                     value_changed)
+        self.changed(code_changed, value_changed)
+            
+    def changed(self, code_changed=False, value_changed=False):
+        """Inform dependents that this shaderobject has changed.
+        """
+        for d in self._dependents:
+            d._dep_changed(self, code_changed=code_changed,
+                           value_changed=value_changed)
     
     def compile(self):
         """ Return a compilation of this object and its dependencies. 


Test patch:
diff --git a/vispy/visuals/shaders/tests/test_function.py b/vispy/visuals/shaders/tests/test_function.py
--- a/vispy/visuals/shaders/tests/test_function.py
+++ b/vispy/visuals/shaders/tests/test_function.py
@@ -314,8 +314,10 @@ def test_function_basics():
 def test_function_changed():
     ch = []
     
-    def on_change(event):
-        ch.append(event.source)
+    class C(object):
+        def _dep_changed(self, dep, **kwargs):
+            ch.append(dep)
+    ch_obj = C()
         
     def assert_changed(*objs):
         assert set(ch) == set(objs)
@@ -323,7 +325,7 @@ def assert_changed(*objs):
             ch.pop()
         
     fun1 = Function('void main(){$var1; $var2;}')
-    fun1.changed.connect(on_change)
+    fun1._dependents[ch_obj] = None
     fun1['var1'] = 'x'
     fun1['var2'] = 'y'
     assert_changed(fun1)
@@ -337,7 +339,7 @@ def assert_changed(*objs):
     
     fun1['var1'] = 0.5
     var1 = fun1['var1']
-    var1.changed.connect(on_change)
+    var1._dependents[ch_obj] = None
     assert_changed(fun1)
     
     var1.name = 'xxx'
@@ -354,7 +356,7 @@ def assert_changed(*objs):
     # test variable disconnect
     fun1['var1'] = Variable('var1', 7)
     var2 = fun1['var1']
-    var2.changed.connect(on_change)
+    var2._dependents[ch_obj] = None
     #assert_changed(fun1)
     # var2 is now connected
     var2.value = (1, 2, 3, 4)
@@ -371,9 +373,9 @@ def assert_changed(*objs):
     fun1['var2'] = exp1
     assert_changed(fun1)
     
-    fun2.changed.connect(on_change)
-    fun3.changed.connect(on_change)
-    exp1.changed.connect(on_change)
+    fun2._dependents[ch_obj] = None
+    fun3._dependents[ch_obj] = None
+    exp1._dependents[ch_obj] = None
     
     fun2['var1'] = 'x'
     assert_changed(fun1, fun2, exp1)