Repository: spyder-ide/spyder

Issue:
QtWebEngineProcess stays open after closing ipython tab
For each ipython tab i open, I get a new QtWebEngineProcess, as expected from:
https://github.com/spyder-ide/spyder/blob/37c9491ca5468d3738377b95305095811c8a1c0e/spyder/widgets/ipythonconsole/client.py#L137

But this process stays open after closing the tab! I strongly believe that closing spyder takes an extra 5-10s because of that.

Disabling the automatic tab opening:
![screen shot 2018-05-07 at 15 29 31](https://user-images.githubusercontent.com/6740194/39707283-e30c6194-520b-11e8-8169-bff0fe0a546e.png)
First I have 1 main Python process, 2 other python process and 2 QtWebEngineProcess.
If I close spyder now it will be fast.
![screen shot 2018-05-07 at 15 30 39](https://user-images.githubusercontent.com/6740194/39707282-e2f63766-520b-11e8-9f6e-eb3467518d93.png)
Then I open 3 tabs. I get 3 extra python process, and 3 extra QtWebEngineProcess. Spyder will need to wait for some kind of timeout before closing (5-15 s usually).
![screen shot 2018-05-07 at 15 31 23](https://user-images.githubusercontent.com/6740194/39707281-e2df47e0-520b-11e8-93dc-45722047ea0c.png)
The three tabs are closed. The 3 extra python process disappeared, but not the QtWebEngineProcess. Spyder is still slow to close.


Python 3.6.5 64bits, Qt 5.9.1, PyQt5 5.9 on Darwin


Discussion:
I really don't know how we could fix this other than removing the info widget. Google doesn't return anything useful.

@impact27, thoughts?
This looks like a reference cycle. I thinks the ipython client never gets deleted because reasons?
I will see if I find something.
![client](https://user-images.githubusercontent.com/6740194/39861266-f82041f8-5437-11e8-8d43-88220529d033.png)

exit_callback might make a loop and ShellWidget has 4 references to client, mostly from lambda functions.

Resolving patch:
diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -26,6 +26,8 @@
 from qtconsole.client import QtKernelClient
 from qtconsole.manager import QtKernelManager
 from qtpy.QtCore import Qt, Signal, Slot
+from qtpy.QtGui import QColor
+from qtpy.QtWebEngineWidgets import WEBENGINE
 from qtpy.QtWidgets import (QActionGroup, QApplication, QGridLayout,
                             QGroupBox, QHBoxLayout, QLabel, QMenu, QMessageBox,
                             QTabWidget, QVBoxLayout, QWidget)
@@ -35,6 +37,7 @@
 # Local imports
 from spyder import dependencies
 from spyder.config.base import _, get_conf_path, get_home_dir
+from spyder.config.gui import get_font, is_dark_interface
 from spyder.config.main import CONF
 from spyder.api.plugins import SpyderPluginWidget
 from spyder.py3compat import is_string, PY2, to_text_string
@@ -50,6 +53,7 @@
 from spyder.widgets.findreplace import FindReplace
 from spyder.plugins.ipythonconsole.widgets import ClientWidget
 from spyder.plugins.ipythonconsole.widgets import KernelConnectionDialog
+from spyder.widgets.browser import WebView
 from spyder.widgets.tabs import Tabs
 
 
@@ -74,6 +78,11 @@
 dependencies.add("matplotlib", _("Display 2D graphics in the IPython Console"),
                  required_version=MATPLOTLIB_REQVER, optional=True)
 
+if is_dark_interface():
+    MAIN_BG_COLOR = '#19232D'
+else:
+    MAIN_BG_COLOR = 'white'
+
 
 class IPythonConsole(SpyderPluginWidget):
     """
@@ -146,6 +155,16 @@ def __init__(self, parent, test_dir=None, test_no_stderr=False,
         else:
             layout.addWidget(self.tabwidget)
 
+        # Info widget
+        self.infowidget = WebView(self)
+        if WEBENGINE:
+            self.infowidget.page().setBackgroundColor(QColor(MAIN_BG_COLOR))
+        else:
+            self.infowidget.setStyleSheet(
+                "background:{}".format(MAIN_BG_COLOR))
+        self.set_infowidget_font()
+        layout.addWidget(self.infowidget)
+
         # Find/replace widget
         self.find_widget = FindReplace(self)
         self.find_widget.hide()
@@ -244,10 +263,24 @@ def refresh_plugin(self):
         """Refresh tabwidget"""
         client = None
         if self.tabwidget.count():
-            # Give focus to the control widget of the selected tab
             client = self.tabwidget.currentWidget()
+
+            # Decide what to show for each client
+            if client.info_page != client.blank_page:
+                # Show info_page if it has content
+                client.set_info_page()
+                client.shellwidget.hide()
+                client.layout.addWidget(self.infowidget)
+                self.infowidget.show()
+            else:
+                self.infowidget.hide()
+                client.shellwidget.show()
+
+            # Give focus to the control widget of the selected tab
             control = client.get_control()
             control.setFocus()
+
+            # Create corner widgets
             buttons = [[b, -7] for b in client.get_toolbar_buttons()]
             buttons = sum(buttons, [])[:-1]
             widgets = [client.create_time_label()] + buttons
@@ -1075,6 +1108,11 @@ def set_elapsed_time(self, client):
                 client.timer.start(1000)
                 break
 
+    def set_infowidget_font(self):
+        """Set font for infowidget"""
+        font = get_font(option='rich_font')
+        self.infowidget.set_font(font)
+
     #------ Public API (for kernels) ------------------------------------------
     def ssh_tunnel(self, *args, **kwargs):
         if os.name == 'nt':
diff --git a/spyder/plugins/ipythonconsole/widgets/client.py b/spyder/plugins/ipythonconsole/widgets/client.py
--- a/spyder/plugins/ipythonconsole/widgets/client.py
+++ b/spyder/plugins/ipythonconsole/widgets/client.py
@@ -26,15 +26,14 @@
 
 # Third party imports (qtpy)
 from qtpy.QtCore import QUrl, QTimer, Signal, Slot
-from qtpy.QtGui import QKeySequence, QColor
+from qtpy.QtGui import QKeySequence
 from qtpy.QtWidgets import (QHBoxLayout, QLabel, QMenu, QMessageBox,
                             QToolButton, QVBoxLayout, QWidget)
-from qtpy.QtWebEngineWidgets import WEBENGINE
 
 # Local imports
 from spyder.config.base import (_, get_image_path, get_module_source_path,
                                 running_under_pytest)
-from spyder.config.gui import get_font, get_shortcut, is_dark_interface
+from spyder.config.gui import get_shortcut, is_dark_interface
 from spyder.utils import icon_manager as ima
 from spyder.utils import sourcecode
 from spyder.utils.encoding import get_coding
@@ -44,7 +43,6 @@
                                     create_toolbutton, DialogManager,
                                     MENU_SEPARATOR)
 from spyder.py3compat import to_text_string
-from spyder.widgets.browser import WebView
 from spyder.plugins.ipythonconsole.widgets import ShellWidget
 from spyder.widgets.mixins import SaveHistoryMixin
 from spyder.plugins.variableexplorer.widgets.collectionseditor import (
@@ -65,11 +63,6 @@
 LOADING = open(osp.join(TEMPLATES_PATH, 'loading.html')).read()
 KERNEL_ERROR = open(osp.join(TEMPLATES_PATH, 'kernel_error.html')).read()
 
-if is_dark_interface():
-    MAIN_BG_COLOR = '#19232D'
-else:
-    MAIN_BG_COLOR = 'white'
-
 try:
     time.monotonic  # time.monotonic new in 3.3
 except AttributeError:
@@ -97,8 +90,8 @@ class ClientWidget(QWidget, SaveHistoryMixin):
     """
     Client widget for the IPython Console
 
-    This is a widget composed of a shell widget and a WebView info widget
-    to print different messages there.
+    This widget is necessary to handle the interaction between the
+    plugin and each shell widget.
     """
 
     SEPARATOR = '{0}## ---({1})---'.format(os.linesep*2, time.ctime())
@@ -155,15 +148,13 @@ def __init__(self, plugin, id_,
                                        interpreter_versions=interpreter_versions,
                                        external_kernel=external_kernel,
                                        local_kernel=True)
-        self.infowidget = WebView(self)
-        if WEBENGINE:
-            self.infowidget.page().setBackgroundColor(QColor(MAIN_BG_COLOR))
-        else:
-            self.infowidget.setStyleSheet(
-                "background:{}".format(MAIN_BG_COLOR))
-        self.set_infowidget_font()
+
+        self.infowidget = plugin.infowidget
         self.blank_page = self._create_blank_page()
         self.loading_page = self._create_loading_page()
+        # To keep a reference to the page to be displayed
+        # in infowidget
+        self.info_page = None
         self._show_loading_page()
 
         # Elapsed time
@@ -174,7 +165,7 @@ def __init__(self, plugin, id_,
                                          toggled=self.set_elapsed_time_visible)
 
         # --- Layout
-        vlayout = QVBoxLayout()
+        self.layout = QVBoxLayout()
         toolbar_buttons = self.get_toolbar_buttons()
 
         hlayout = QHBoxLayout()
@@ -183,11 +174,11 @@ def __init__(self, plugin, id_,
         for button in toolbar_buttons:
             hlayout.addWidget(button)
 
-        vlayout.addLayout(hlayout)
-        vlayout.setContentsMargins(0, 0, 0, 0)
-        vlayout.addWidget(self.shellwidget)
-        vlayout.addWidget(self.infowidget)
-        self.setLayout(vlayout)
+        self.layout.addLayout(hlayout)
+        self.layout.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.shellwidget)
+        self.layout.addWidget(self.infowidget)
+        self.setLayout(self.layout)
 
         # --- Exit function
         self.exit_callback = lambda: plugin.close_client(client=self)
@@ -334,12 +325,13 @@ def show_kernel_error(self, error):
         # Create error page
         message = _("An error ocurred while starting the kernel")
         kernel_error_template = Template(KERNEL_ERROR)
-        page = kernel_error_template.substitute(css_path=self.css_path,
-                                                message=message,
-                                                error=error)
+        self.info_page = kernel_error_template.substitute(
+            css_path=self.css_path,
+            message=message,
+            error=error)
 
         # Show error
-        self.infowidget.setHtml(page, QUrl.fromLocalFile(self.css_path))
+        self.set_info_page()
         self.shellwidget.hide()
         self.infowidget.show()
 
@@ -491,11 +483,6 @@ def set_font(self, font):
         self.shellwidget._control.setFont(font)
         self.shellwidget.font = font
 
-    def set_infowidget_font(self):
-        """Set font for infowidget"""
-        font = get_font(option='rich_font')
-        self.infowidget.set_font(font)
-
     def set_color_scheme(self, color_scheme, reset=True):
         """Set IPython color scheme."""
         # Needed to handle not initialized kernel_client
@@ -667,6 +654,14 @@ def set_elapsed_time_visible(self, state):
         if self.time_label is not None:
             self.time_label.setVisible(state)
 
+    def set_info_page(self):
+        """Set current info_page."""
+        if self.info_page is not None:
+            self.infowidget.setHtml(
+                self.info_page,
+                QUrl.fromLocalFile(self.css_path)
+            )
+
     #------ Private API -------------------------------------------------------
     def _create_loading_page(self):
         """Create html page to show while the kernel is starting"""
@@ -690,15 +685,15 @@ def _show_loading_page(self):
         """Show animation while the kernel is loading."""
         self.shellwidget.hide()
         self.infowidget.show()
-        self.infowidget.setHtml(self.loading_page,
-                                QUrl.fromLocalFile(self.css_path))
+        self.info_page = self.loading_page
+        self.set_info_page()
 
     def _hide_loading_page(self):
         """Hide animation shown while the kernel is loading."""
         self.infowidget.hide()
         self.shellwidget.show()
-        self.infowidget.setHtml(self.blank_page,
-                                QUrl.fromLocalFile(self.css_path))
+        self.info_page = self.blank_page
+        self.set_info_page()
         self.shellwidget.sig_prompt_ready.disconnect(self._hide_loading_page)
 
     def _read_stderr(self):


Test patch: