Repository: biolab/orange3

Issue:
Correlations makes Orange unresponsive
**Describe the bug**
When given data with many features, Correlations becomes unresponsive after a minute or two of runtime.

**To Reproduce**
Take GDS100 data set from GEO Data Sets (bioinformatics add on), samples in rows.

![image](https://user-images.githubusercontent.com/726604/64416765-19533280-d098-11e9-892c-22902f57de55.png)

![image](https://user-images.githubusercontent.com/726604/64416791-2708b800-d098-11e9-9ac7-d1c776b62082.png)

Correlations widget is responsive at the start, but then freezes Orage. Clicking on Pause has no effect.

**Expected behavior**
The widget should be responsive and also show the progress bar, even when there are many features on the input.


Discussion:

Resolving patch:
diff --git a/Orange/widgets/data/owcorrelations.py b/Orange/widgets/data/owcorrelations.py
--- a/Orange/widgets/data/owcorrelations.py
+++ b/Orange/widgets/data/owcorrelations.py
@@ -196,6 +196,38 @@ def stopped(self):
         header = self.rank_table.horizontalHeader()
         header.setSectionResizeMode(1, QHeaderView.Stretch)
 
+    def start(self, task, *args, **kwargs):
+        self.__set_state_ready()
+        super().start(task, *args, **kwargs)
+        self.__set_state_busy()
+
+    def cancel(self):
+        super().cancel()
+        self.__set_state_ready()
+
+    def _connect_signals(self, state):
+        super()._connect_signals(state)
+        state.progress_changed.connect(self.master.progressBarSet)
+        state.status_changed.connect(self.master.setStatusMessage)
+
+    def _disconnect_signals(self, state):
+        super()._disconnect_signals(state)
+        state.progress_changed.disconnect(self.master.progressBarSet)
+        state.status_changed.disconnect(self.master.setStatusMessage)
+
+    def _on_task_done(self, future):
+        super()._on_task_done(future)
+        self.__set_state_ready()
+
+    def __set_state_ready(self):
+        self.master.progressBarFinished()
+        self.master.setBlocking(False)
+        self.master.setStatusMessage("")
+
+    def __set_state_busy(self):
+        self.master.progressBarInit()
+        self.master.setBlocking(True)
+
 
 class OWCorrelations(OWWidget):
     name = "Correlations"
@@ -250,7 +282,6 @@ def __init__(self):
 
         self.vizrank, _ = CorrelationRank.add_vizrank(
             None, self, None, self._vizrank_selection_changed)
-        self.vizrank.progressBar = self.progressBar
         self.vizrank.button.setEnabled(False)
         self.vizrank.threadStopped.connect(self._vizrank_stopped)
 
diff --git a/Orange/widgets/visualize/owlinearprojection.py b/Orange/widgets/visualize/owlinearprojection.py
--- a/Orange/widgets/visualize/owlinearprojection.py
+++ b/Orange/widgets/visualize/owlinearprojection.py
@@ -59,6 +59,7 @@ def __init__(self, master):
         gui.rubber(box)
         self.last_run_n_attrs = None
         self.attr_color = master.attr_color
+        self.attrs = []
 
     def initialize(self):
         super().initialize()
@@ -95,6 +96,8 @@ def check_preconditions(self):
 
     def state_count(self):
         n_all_attrs = len(self.attrs)
+        if not n_all_attrs:
+            return 0
         n_attrs = self.n_attrs
         return factorial(n_all_attrs) // (2 * factorial(n_all_attrs - n_attrs) * n_attrs)
 
diff --git a/Orange/widgets/visualize/utils/__init__.py b/Orange/widgets/visualize/utils/__init__.py
--- a/Orange/widgets/visualize/utils/__init__.py
+++ b/Orange/widgets/visualize/utils/__init__.py
@@ -6,7 +6,8 @@
 from operator import attrgetter
 from queue import Queue, Empty
 from types import SimpleNamespace as namespace
-from typing import Optional, Iterable, List, Callable, Iterator
+from typing import Optional, Iterable, List, Callable
+from threading import Timer
 
 from AnyQt.QtCore import Qt, QSize, pyqtSignal as Signal, QSortFilterProxyModel
 from AnyQt.QtGui import QStandardItemModel, QStandardItem, QColor, QBrush, QPen
@@ -366,7 +367,8 @@ def toggle(self):
             self.progressBarInit()
             self.before_running()
             self.start(run_vizrank, self.compute_score,
-                       self.iterate_states(self.saved_state), self.scores)
+                       self.iterate_states, self.saved_state, self.scores,
+                       self.saved_progress, self.state_count())
         else:
             self.button.setText("Continue")
             self.button.repaint()
@@ -382,10 +384,17 @@ def stopped(self):
         pass
 
 
-def run_vizrank(compute_score: Callable, states: Iterator,
-                scores: List, task: TaskState):
+def run_vizrank(compute_score: Callable, iterate_states: Callable,
+                saved_state: Optional[Iterable], scores: List,
+                progress: int, state_count: int, task: TaskState):
+    task.set_status("Getting combinations...")
+    task.set_progress_value(0.1)
+    states = iterate_states(saved_state)
+
+    task.set_status("Getting scores...")
     res = Result(queue=Queue(), scores=None)
     scores = scores.copy()
+    can_set_partial_result = True
 
     def do_work(st, next_st):
         try:
@@ -398,7 +407,10 @@ def do_work(st, next_st):
         except Exception:  # ignore current state in case of any problem
             pass
         res.scores = scores.copy()
-        task.set_partial_result(res)
+
+    def reset_flag():
+        nonlocal can_set_partial_result
+        can_set_partial_result = True
 
     state = None
     next_state = next(states)
@@ -406,11 +418,23 @@ def do_work(st, next_st):
         while True:
             if task.is_interruption_requested():
                 return res
+            task.set_progress_value(int(progress * 100 / max(1, state_count)))
+            progress += 1
             state = copy.copy(next_state)
             next_state = copy.copy(next(states))
             do_work(state, next_state)
+            # for simple scores (e.g. correlations widget) and many feature
+            # combinations, the 'partial_result_ready' signal (emitted by
+            # invoking 'task.set_partial_result') was emitted too frequently
+            # for a longer period of time and therefore causing the widget
+            # being unresponsive
+            if can_set_partial_result:
+                task.set_partial_result(res)
+                can_set_partial_result = False
+                Timer(0.01, reset_flag).start()
     except StopIteration:
         do_work(state, None)
+        task.set_partial_result(res)
     return res
 
 


Test patch:
diff --git a/Orange/widgets/data/tests/test_owcorrelations.py b/Orange/widgets/data/tests/test_owcorrelations.py
--- a/Orange/widgets/data/tests/test_owcorrelations.py
+++ b/Orange/widgets/data/tests/test_owcorrelations.py
@@ -229,6 +229,7 @@ def test_feature_combo(self):
                            if attr.is_continuous]
         self.assertEqual(len(feature_combo.model()), len(cont_attributes) + 1)
 
+        self.wait_until_stop_blocking()
         self.send_signal(self.widget.Inputs.data, Table("housing"))
         self.assertEqual(len(feature_combo.model()), 14)
 
@@ -281,6 +282,7 @@ def test_send_report(self):
         """Test report """
         self.send_signal(self.widget.Inputs.data, self.data_cont)
         self.widget.report_button.click()
+        self.wait_until_stop_blocking()
         self.send_signal(self.widget.Inputs.data, None)
         self.widget.report_button.click()
 
diff --git a/Orange/widgets/visualize/tests/test_owlinearprojection.py b/Orange/widgets/visualize/tests/test_owlinearprojection.py
--- a/Orange/widgets/visualize/tests/test_owlinearprojection.py
+++ b/Orange/widgets/visualize/tests/test_owlinearprojection.py
@@ -203,13 +203,15 @@ def setUp(self):
     def test_discrete_class(self):
         self.send_signal(self.widget.Inputs.data, self.data)
         run_vizrank(self.vizrank.compute_score,
-                    self.vizrank.iterate_states(None), [], Mock())
+                    self.vizrank.iterate_states, None,
+                    [], 0, self.vizrank.state_count(), Mock())
 
     def test_continuous_class(self):
         data = Table("housing")[::100]
         self.send_signal(self.widget.Inputs.data, data)
         run_vizrank(self.vizrank.compute_score,
-                    self.vizrank.iterate_states(None), [], Mock())
+                    self.vizrank.iterate_states, None,
+                    [], 0, self.vizrank.state_count(), Mock())
 
     def test_set_attrs(self):
         self.send_signal(self.widget.Inputs.data, self.data)
diff --git a/Orange/widgets/visualize/tests/test_vizrankdialog.py b/Orange/widgets/visualize/tests/test_vizrankdialog.py
--- a/Orange/widgets/visualize/tests/test_vizrankdialog.py
+++ b/Orange/widgets/visualize/tests/test_vizrankdialog.py
@@ -31,7 +31,8 @@ def test_run_vizrank(self):
         # run through all states
         task.is_interruption_requested.return_value = False
         states = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
-        res = run_vizrank(compute_score, chain(states), scores, task)
+        res = run_vizrank(compute_score, lambda initial: chain(states),
+                          None, scores, 0, 6, task)
 
         next_state = self.assertQueueEqual(
             res.queue, [0, 0, 0, 3, 2, 5], compute_score,
@@ -40,7 +41,8 @@ def test_run_vizrank(self):
         res_scores = sorted([compute_score(x) for x in states])
         self.assertListEqual(res.scores, res_scores)
         self.assertIsNot(scores, res.scores)
-        self.assertEqual(task.set_partial_result.call_count, 6)
+        self.assertEqual(task.set_partial_result.call_count, 2)
+        self.assertEqual(task.set_progress_value.call_count, 7)
 
     def test_run_vizrank_interrupt(self):
         scores, task = [], Mock()
@@ -48,7 +50,8 @@ def test_run_vizrank_interrupt(self):
         task.is_interruption_requested.side_effect = lambda: \
             True if task.is_interruption_requested.call_count > 2 else False
         states = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
-        res = run_vizrank(compute_score, chain(states), scores, task)
+        res = run_vizrank(compute_score, lambda initial: chain(states),
+                          None, scores, 0, 6, task)
 
         next_state = self.assertQueueEqual(
             res.queue, [0, 0], compute_score, states[:2], states[1:3])
@@ -56,12 +59,15 @@ def test_run_vizrank_interrupt(self):
         res_scores = sorted([compute_score(x) for x in states[:2]])
         self.assertListEqual(res.scores, res_scores)
         self.assertIsNot(scores, res.scores)
-        self.assertEqual(task.set_partial_result.call_count, 2)
+        self.assertEqual(task.set_partial_result.call_count, 1)
+        self.assertEqual(task.set_progress_value.call_count, 3)
+        task.set_progress_value.assert_called_with(int(1 / 6 * 100))
 
         # continue calculation through all states
         task.is_interruption_requested.side_effect = lambda: False
         i = states.index(next_state)
-        res = run_vizrank(compute_score, chain(states[i:]), res_scores, task)
+        res = run_vizrank(compute_score, lambda initial: chain(states[i:]),
+                          None, res_scores, 2, 6, task)
 
         next_state = self.assertQueueEqual(
             res.queue, [0, 3, 2, 5], compute_score, states[2:],
@@ -70,7 +76,9 @@ def test_run_vizrank_interrupt(self):
         res_scores = sorted([compute_score(x) for x in states])
         self.assertListEqual(res.scores, res_scores)
         self.assertIsNot(scores, res.scores)
-        self.assertEqual(task.set_partial_result.call_count, 6)
+        self.assertEqual(task.set_partial_result.call_count, 3)
+        self.assertEqual(task.set_progress_value.call_count, 8)
+        task.set_progress_value.assert_called_with(int(5 / 6 * 100))
 
     def assertQueueEqual(self, queue, positions, f, states, next_states):
         self.assertIsInstance(queue, Queue)
@@ -95,8 +103,12 @@ def iterate_states(initial_state):
         def invoke_on_partial_result():
             widget.on_partial_result(run_vizrank(
                 widget.compute_score,
-                widget.iterate_states(widget.saved_state),
-                widget.scores, task
+                widget.iterate_states,
+                widget.saved_state,
+                widget.scores,
+                widget.saved_progress,
+                widget.state_count(),
+                task
             ))
 
         task = Mock()
@@ -107,6 +119,7 @@ def invoke_on_partial_result():
         widget.compute_score = compute_score
         widget.iterate_states = iterate_states
         widget.row_for_state = lambda sc, _: [QStandardItem(str(sc))]
+        widget.state_count = lambda: len(states)
 
         # interrupt calculation in third iteration
         task.is_interruption_requested.side_effect = lambda: \
@@ -117,6 +130,7 @@ def invoke_on_partial_result():
                 sorted([compute_score(x) for x in states[:2]])):
             self.assertEqual(widget.rank_model.item(row, 0).text(), str(score))
         self.assertEqual(widget.saved_progress, 2)
+        task.set_progress_value.assert_called_with(int(1 / 6 * 100))
 
         # continue calculation through all states
         task.is_interruption_requested.side_effect = lambda: False
@@ -126,6 +140,7 @@ def invoke_on_partial_result():
                 sorted([compute_score(x) for x in states])):
             self.assertEqual(widget.rank_model.item(row, 0).text(), str(score))
         self.assertEqual(widget.saved_progress, 6)
+        task.set_progress_value.assert_called_with(int(5 / 6 * 100))
 
 
 if __name__ == "__main__":