Repository: scipp/plopp

Issue:
Implement caching in graph nodes
Currently, when a change in a node triggers an update in multiple nodes below, all nodes generate a request that travels all the way to the top of the node tree.
This can make things very slow.
We should implement some caching mecanism where a node only get data from its parents if it is not up to date.
This should significantly improve the performance of some interactions.

Discussion:

Patch:
diff --git a/src/plopp/core/graph.py b/src/plopp/core/graph.py
index 5c4d1f9..12ff700 100644
--- a/src/plopp/core/graph.py
+++ b/src/plopp/core/graph.py
@@ -18,7 +18,7 @@ def _make_graphviz_digraph(*args, **kwargs):
 def _add_graph_edges(dot, node, inventory, hide_views):
     name = node.id
     inventory.append(name)
-    dot.node(name, label=escape(str(node.func)))
+    dot.node(name, label=escape(str(node.func)) + '\nid = ' + name)
     for child in node.children:
         key = child.id
         if key not in inventory:
diff --git a/src/plopp/core/model.py b/src/plopp/core/model.py
index 6db54de..db33f0e 100644
--- a/src/plopp/core/model.py
+++ b/src/plopp/core/model.py
@@ -19,6 +19,7 @@ class Node:
         self.kwparents = dict(kwparents)
         for parent in chain(self.parents, self.kwparents.values()):
             parent.add_child(self)
+        self._data = None
 
     def remove(self):
         if self.children:
@@ -33,9 +34,14 @@ class Node:
         self.kwparents.clear()
 
     def request_data(self):
-        args = (parent.request_data() for parent in self.parents)
-        kwargs = {key: parent.request_data() for key, parent in self.kwparents.items()}
-        return self.func(*args, **kwargs)
+        if self._data is None:
+            args = (parent.request_data() for parent in self.parents)
+            kwargs = {
+                key: parent.request_data()
+                for key, parent in self.kwparents.items()
+            }
+            self._data = self.func(*args, **kwargs)
+        return self._data
 
     def add_child(self, child):
         self.children.append(child)
@@ -44,6 +50,7 @@ class Node:
         self.views.append(view)
 
     def notify_children(self, message):
+        self._data = None
         self.notify_views(message)
         for child in self.children:
             child.notify_children(message)


Test patch:
diff --git a/tests/core/model_test.py b/tests/core/model_test.py
index 21d65bc..4ae1189 100644
--- a/tests/core/model_test.py
+++ b/tests/core/model_test.py
@@ -2,6 +2,7 @@
 # Copyright (c) 2022 Scipp contributors (https://github.com/scipp)
 
 from plopp import Node, node, View
+from functools import partial
 import pytest
 
 
@@ -96,6 +97,34 @@ def test_two_children_request_data():
     assert cv.data == 7
 
 
+def test_data_request_is_cached():
+    global log
+    log = ''
+
+    def log_and_call(n, f, x=None):
+        global log
+        log += n
+        if x is None:
+            return f()
+        else:
+            return f(x)
+
+    a = Node(partial(log_and_call, n='a', f=lambda: 5))
+    b = node(partial(log_and_call, n='b', f=lambda x: x - 2))(x=a)
+    c = node(partial(log_and_call, n='c', f=lambda x: x + 2))(x=a)
+    d = node(partial(log_and_call, n='d', f=lambda x: x**2))(x=c)
+    av = DataView(a)  # noqa: F841
+    bv = DataView(b)  # noqa: F841
+    cv = DataView(c)  # noqa: F841
+    dv = DataView(d)  # noqa: F841
+
+    a.notify_children(message='hello from a')
+    assert log == 'abcd'  # 'a' should only appear once in the log
+    log = ''
+    c.notify_children(message='hello from c')
+    assert log == 'cd'  # 'c' requests data from 'a' but 'a' is cached so no 'a' in log
+
+
 def test_remove_node():
     a = Node(lambda: 5)
     b = node(lambda x: x - 2)(x=a)