Repository: julien6387/supvisors (language: python)

Issue:
add possibility to export statistics
export using the event interface



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f5c87554..8323d1f7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,16 +4,17 @@ on: [push, pull_request]
 
 jobs:
   tests:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     strategy:
+      fail-fast: false
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
 
@@ -30,15 +31,17 @@ jobs:
       run: tox
 
   coverage:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
+    strategy:
+      fail-fast: false
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
-        python-version: '3.6'
+        python-version: '3.9'
 
     - name: Install dependencies
       run: pip install virtualenv tox
@@ -54,15 +57,15 @@ jobs:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
   docs:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
-        python-version: '3.6'
+        python-version: '3.8'
 
     - name: Install dependencies
       run: pip install virtualenv tox
diff --git a/CHANGES.md b/CHANGES.md
index c8ed2d28..d82561d7 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,48 @@
 # Change Log
 
+## 0.16 (2023-03-12)
+
+* Add `websockets` as an option to the **Supvisors** event listener (Python 3.7+ only).
+
+* Re-design the `PyZMQ` event listener using the `zmq.asyncio` support for better commonalities
+  with the `wesockets` solution.
+
+* Re-design the statistics collection and compilation.
+
+* The option `stats_enabled` takes additional values to control host and process statistics independently.
+
+* The option `stats_collecting_period` has been added to set the minimum time between process statistics collection.
+
+* The option `stats_periods` accepts float values, not necessarily multiples of 5.
+
+* Fix [Issue #54](https://github.com/julien6387/supvisors/issues/54).
+  Add host and process statistics to the **Supvisors** event interface.
+
+* Fix children process CPU times in statistics.
+
+* Fix Solaris mode not taken into account for the process mean CPU value in the **Supvisors** Web UI.
+
+* Only one **Supvisors** instance is running when both `unix_http_server` and `inet_http_server` sections are defined
+  in the supervisor configuration file.
+
+* Fix Flask `start_args` to pass the extra arguments in the URL attributes rather than in the route.
+
+* The local **Supvisors** instance is identified as the item having the same fully qualified domain name
+  (as returned by `socket.gethostaddr` and `socket.getfqdn`) among the items of the `supvisors_list` option. 
+
+* Use the HTTP server port to help the identification of the local **Supvisors** instance when multiple items
+  of the `supvisors_list` option fit and identifier is not set.
+
+* The attribute `process_failure` is added to the `get_instance_info` XML-RPC to inform if there is a process failure
+  in the **Supvisors** instance. The attribute is also provided in the event interface and in the `instance_status`
+  option of the `supervisorctl` command. 
+
+* Raise an exception when the matching **Supvisors** instance in the `supvisors_list` option is inconsistent
+  with the local configuration.
+
+* Add a **Supvisors** logo.
+
+
 ## 0.15 (2022-11-20)
 
 * Publish / Subscribe pattern implemented for **Supvisors** internal communication.
diff --git a/MANIFEST.in b/MANIFEST.in
index 8d73e416..e7a11bfa 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,7 +1,7 @@
-include CHANGES.txt
+include CHANGES.md
 include LICENSE
-include README.rst
 include supvisors/version.txt
 include supvisors/rules.xsd
 graft supvisors/doc
 graft supvisors/ui
+prune supvisors/ui/img/model
diff --git a/README.md b/README.md
index 9518920e..6f95fb61 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ The main features are:
 * a new web-based dashboard that replaces the default dashboard of Supervisor and allows to control
   all the Supervisor instances declared,
 * an extended XML-RPC API to control applications and processes over the multiple Supervisor instances,
-* a notification interface to get the events from multiple Supervisor instances on a `PyZmq` socket. 
+* a notification interface to get the events from multiple Supervisor instances on a `websocket` or on a `PyZmq` socket.
 
 A set of application and program rules can be added to manage:
 * the starting sequence of the applications,
@@ -68,11 +68,12 @@ but is not maintained anymore.
 | Package                                           | Release | Optional |
 |---------------------------------------------------|---------|----------|
 | [Supervisor](http://supervisord.org)              | 4.2.4   |          |
-| [PyZMQ](http://pyzmq.readthedocs.io)              | 20.0.0  | X        |
 | [psutil](https://pypi.python.org/pypi/psutil)     | 5.7.3   | X        |
 | [matplotlib](http://matplotlib.org)               | 3.3.3   | X        |
 | [lxml](http://lxml.de)                            | 4.6.2   | X        |
 | [Flask-RESTX](https://flask-restx.readthedocs.io) | 0.5.1   | X        |
+| [PyZMQ](http://pyzmq.readthedocs.io)              | 20.0.0  | X        |
+| [websockets](https://websockets.readthedocs.io)   | 10.4    | X        |
 
 Please note that some of these dependencies may have their own dependencies.
 
diff --git a/docs/common.rst b/docs/common.rst
index b4cb89e8..75dba44e 100644
--- a/docs/common.rst
+++ b/docs/common.rst
@@ -1,8 +1,12 @@
 .. |Supvisors| replace:: **Supvisors**
 .. |Supervisor| replace:: Supervisor_
 .. |psutil| replace:: psutil_
+.. |lxml| replace:: lxml_
 .. |Flask-RESTX| replace:: Flask-RESTX_
 .. |Flask| replace:: Flask_
+.. |PyZMQ| replace:: PyZMQ_
+.. |ZeroMQ| replace:: ZeroMQ_
+.. |Websockets| replace:: websockets_
 
 .. |start| image:: images/start_button.png
     :alt: Start button
@@ -23,9 +27,11 @@
     :alt: Refresh button
 
 .. _Supervisor: http://supervisord.org
-.. _PyZMQ: http://pyzmq.readthedocs.io
 .. _psutil: https://pypi.python.org/pypi/psutil
-.. _matplotlib: http://matplotlib.org
-.. _lxml: http://lxml.de
+.. _matplotlib: https://matplotlib.org
+.. _lxml: https://lxml.de
 .. _Flask-RESTX: https://flask-restx.readthedocs.io
 .. _Flask: https://flask.palletsprojects.com
+.. _PyZMQ: https://pyzmq.readthedocs.io
+.. _ZeroMQ: https://zeromq.org
+.. _websockets: https://websockets.readthedocs.io
diff --git a/docs/configuration.rst b/docs/configuration.rst
index b78a09c6..1f054a4f 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -42,7 +42,9 @@ behavior may happen. The present section details where it is applicable.
 ``supvisors_list``
 
     The exhaustive list of |Supvisors| instances to handle, separated by commas.
-    Each element should match the following format: ``<identifier>host_name:http_port:internal_port``,
+    The elements of ``supvisors_list`` define how the |Supvisors| instances will share information between them
+    and must be identical to all |Supvisors| instances or unpredictable behavior may happen.
+    The exhaustive form of an element matches ``<identifier>host_name:http_port:internal_port``,
     where ``identifier`` is the optional but **unique** |Supervisor| identifier (it can be set in the |Supervisor|
     configuration or in the command line when starting the ``supervisord`` program) ;
     ``host_name`` is the name of the node where the |Supvisors| instance is running ;
@@ -50,8 +52,6 @@ behavior may happen. The present section details where it is applicable.
     unique per node) ;
     ``internal_port`` is the port of the socket used by the |Supvisors| instance to publish internal events (also
     unique per node).
-    The value of ``supvisors_list`` defines how the |Supvisors| instances will share information between them and must
-    be identical to all |Supvisors| instances or unpredictable behavior may happen.
 
     *Default*:  the local host name.
 
@@ -82,9 +82,9 @@ behavior may happen. The present section details where it is applicable.
 
     .. important:: *About the deduced names*
 
-        Depending on the value chosen, the *deduced name* of the |Supvisors| instance may vary. As this name is expected
-        to be used in the rules files to define where the processes can be started, it is important to understand how
-        it is built.
+        Depending on the name configured, the *deduced name* of the |Supvisors| instance may vary.
+        As this name is expected to be used in the rules files to define where the processes can be started,
+        it is important to understand how it is built.
 
         As a general rule, ``identifier`` takes precedence as a deduced name when set. Otherwise ``host_name`` is
         used when set alone, unless a ``http_port`` is explicitly defined, in which case ``host_name:http_port``
@@ -148,9 +148,10 @@ behavior may happen. The present section details where it is applicable.
 ``event_link``
 
     The communication protocol type used to publish all |Supvisors| events (Instance, Application and Process events).
-    Value in [``NONE`` ; ``ZMQ``]. Other protocols may be considered in the future.
+    Value in [``NONE`` ; ``ZMQ`` ; ``WS``]. Other protocols may be considered in the future.
     If set to ``NONE``, the interface is not available.
-    If set to ``ZMQ``, events are published through a PyZMQ TCP socket.
+    If set to ``ZMQ``, events are published through a |ZeroMQ| TCP socket.
+    If set to ``WS``, events are published through |Websockets| (requires a :command:`Python` version 3.7 or later).
     The protocol of this interface is detailed in :ref:`event_interface`.
 
     *Default*:  NONE.
@@ -254,18 +255,51 @@ behavior may happen. The present section details where it is applicable.
 
     By default, |Supvisors| can provide basic statistics on the node and the processes spawned by |Supervisor|
     on the |Supvisors| :ref:`dashboard`, provided that the |psutil| module is installed.
-    This option can be used to disable the collection of the statistics.
+    This option can be used to adjust or disable the collection of the host and/or process statistics.
+    Possible values are in { ``OFF``, ``HOST``, ``PROCESS``, ``ALL`` }.
+    For backwards compatibility, boolean values ``true`` and ``false`` have been kept and are respectively equal to
+    ``ALL`` and ``OFF``.
 
-    *Default*:  ``true``.
+    *Default*:  ``ALL``.
 
     *Required*:  No.
 
     *Identical*:  No.
 
+``stats_collecting_period``
+
+    This is the *minimum* duration between 2 statistics measurements on one process. It is not a strict period.
+    Value in [``1`` ; ``3600``] seconds.
+
+    *Default*:  ``10``.
+
+    *Required*:  No.
+
+    *Identical*:  No.
+
+    .. note::
+
+        The process statistics collection is deferred to a dedicated process of |Supvisors|. The *Collector* is mainly
+        using the |psutil| module.
+        Regardless of the number of processes to manage, the *Collector* will not take up more than one processor core.
+        If there are more statistics requests than allowed by one processor core, the duration between 2 measurements
+        on the same process will increase automatically.
+
+        If there are *many* processes to deal with and if it is unsuitable to dedicate one whole processor core
+        to process statistics, the ``stats_collecting_period`` should be increased or the process statistics
+        may be deactivated using the the ``stats_enabled`` option.
+
+    .. attention::
+
+        If there are multiple |Supvisors| instances on the same host, there will be multiple *Collector* processes too.
+        The user should pay attention to set the ``stats_collecting_period`` option accordingly so that the CPU load
+        remains within acceptable limits.
+
+
 ``stats_periods``
 
     The list of periods for which the statistics will be provided in the |Supvisors| :ref:`dashboard`, separated by
-    commas. Up to 3 values are allowed in [``5`` ; ``3600``] seconds, each of them MUST be a multiple of 5.
+    commas. Up to 3 int or float values are allowed in [``1`` ; ``3600``] seconds.
 
     *Default*:  ``10``.
 
@@ -424,6 +458,7 @@ Configuration File Example
     rules_files = ./etc/my_movies*.xml
     auto_fence = false
     internal_port = 60001
+    event_link = WS
     event_port = 60002
     synchro_timeout = 20
     inactivity_ticks = 3
@@ -467,7 +502,7 @@ and the quality of service expected. It relies on the |Supervisor| group and pro
     There can be as many running instances of the same program as |Supervisor| allows over the available |Supvisors|
     instances.
 
-If the `lxml <http://lxml.de>`_ package is available on the system, |Supvisors| uses it to validate the XML rules files
+If the |lxml| package is available on the system, |Supvisors| uses it to validate the XML rules files
 before they are used.
 
 .. hint::
@@ -642,8 +677,8 @@ Here follows the definition of the attributes and rules applicable to an ``appli
 ``<program>`` rules
 ~~~~~~~~~~~~~~~~~~~
 
-The ``program`` element defines the rules applicable to at least one program. This element should be included in an
-``programs`` element. *DEPRECATED* It can be also directly included in an ``application`` element.
+The ``program`` element defines the rules applicable to at least one program. This element must be included in an
+``programs`` element.
 Here follows the definition of the attributes and rules applicable to this element.
 
 .. note::
diff --git a/docs/dashboard.rst b/docs/dashboard.rst
index 36fe3087..05ce6cef 100644
--- a/docs/dashboard.rst
+++ b/docs/dashboard.rst
@@ -31,9 +31,9 @@ Common Menu
     :alt: Common Menu
     :align: center
 
-Clicking on the 'Supvisors' title brings the `Main page`_ back or the `Conciliation page`_ if it blinks in red.
-The version of |Supvisors| is displayed underneath. There's also a reminder of the |Supvisors| instance that provides
-the information.
+Clicking on the 'Supvisors' title brings the `Main page`_ back or the `Conciliation page`_ if a red light is blinking
+next to it. The version of |Supvisors| is displayed underneath. There's also a reminder of the |Supvisors| instance
+that provides the information.
 
 Below is the **Supervisors** part that lists all the |Supvisors| instances defined in the :ref:`supvisors_section`
 of the |Supervisor| configuration file.
@@ -198,13 +198,13 @@ Each box contains:
 Conciliation Page
 -----------------
 
-If the page is refreshed when |Supvisors| is in ``CONCILIATION`` state, the 'Supvisors' label in the top left
-of the `Common Menu`_ becomes red and blinks.
+If the page is refreshed when |Supvisors| is in ``CONCILIATION`` state, a red light is blinking next to the 'Supvisors'
+title in the top left of the `Common Menu`_.
 This situation is unlikely to happen if the ``conciliation_strategy`` chosen in the :ref:`supvisors_section`
 of the |Supervisor| configuration file is different from ``USER``, as the other values will trigger an immediate and
 automatic conciliation of the conflicts.
 
-The Conciliation Page can be reached by clicking on this blinking red label.
+The Conciliation Page can be reached by clicking on the 'Supvisors' title when the red blinking light is displayed.
 
 .. image:: images/supvisors_conciliation_page.png
     :alt: Supvisors Conciliation Page
diff --git a/docs/event_interface.rst b/docs/event_interface.rst
index 01db3d60..1f1e97a1 100644
--- a/docs/event_interface.rst
+++ b/docs/event_interface.rst
@@ -3,41 +3,36 @@
 Event interface
 ===============
 
-Protocol
---------
+Available Protocols
+-------------------
 
-The |Supvisors| Event Interface relies on a PyZMQ_ socket.
-To receive the |Supvisors| events, the client application must configure a socket with a ``SUBSCRIBE`` pattern
-and connect it on localhost using the ``event_port`` option defined in the :ref:`supvisors_section` of the |Supervisor|
-configuration file. The ``event_link`` option must also be set to ``ZMQ``.
+The |Supvisors| Event Interface can be created either using a |ZeroMQ| socket or using |websockets|.
 
-|Supvisors| publishes the events in multi-parts messages.
+All messages consist in a header and a body.
+
+.. attention::
 
+    The |websockets| implementation requires a :command:`Python` version 3.7 or later.
 
 Message header
 --------------
 
-The first part is a header that consists in an unicode string. This header identifies the type of the event,
-defined as follows in the ``supvisors.utils`` module:
-
-.. code-block:: python
-
-    SUPVISORS_STATUS_HEADER = u'supvisors'
-    INSTANCE_STATUS_HEADER = u'instance'
-    APPLICATION_STATUS_HEADER = u'application'
-    PROCESS_STATUS_HEADER = u'process'
-    PROCESS_EVENT_HEADER = u'event'
-
-PyZMQ_ makes it possible to filter the messages received on the client side by subscribing to a part of them.
-To receive all messages, just subscribe using an empty string.
-For example, the following lines in python configure the PyZMQ_ socket so as to receive only the ``Supvisors``
-and ``Process`` events:
+This header is a unicode string that identifies the type of the event and that is defined as follows
+in the ``supvisors.ttypes`` module:
 
 .. code-block:: python
 
-    socket.setsockopt(zmq.SUBSCRIBE, SUPVISORS_STATUS_HEADER.encode('utf-8'))
-    socket.setsockopt(zmq.SUBSCRIBE, PROCESS_STATUS_HEADER.encode('utf-8'))
+    class EventHeaders(Enum):
+        """ Strings used as headers in messages between EventPublisher and Supvisors' Client. """
+        SUPVISORS = 'supvisors'
+        INSTANCE = 'instance'
+        APPLICATION = 'application'
+        PROCESS_EVENT = 'event'
+        PROCESS_STATUS = 'process'
+        HOST_STATISTICS = 'hstats'
+        PROCESS_STATISTICS = 'pstats'
 
+The header value is used to set the event subscriptions.
 
 Message data
 ------------
@@ -49,70 +44,75 @@ Of course, the contents depends on the message type.
 |Supvisors| status
 ~~~~~~~~~~~~~~~~~~
 
-================== ==================
-Key	               Value
-================== ==================
-'fsm_statecode'    The state of |Supvisors|, in [0;6].
-'fsm_statename'    The string state of |Supvisors|, among { ``'INITIALIZATION'``, ``'DEPLOYMENT'``, ``'OPERATION'``,
-                   ``'CONCILIATION'``, ``'RESTARTING'``, ``'SHUTTING_DOWN'``, ``'SHUTDOWN'`` }.
-'starting_jobs'    The list of |Supvisors| instances having starting jobs in progress.
-'stopping_jobs'    The list of |Supvisors| instances having stopping jobs in progress.
-================== ==================
+================== ================= ==================
+Key	               Type               Value
+================== ================= ==================
+'fsm_statecode'    ``int``           The state of |Supvisors|, in [0;6].
+'fsm_statename'    ``str``           The string state of |Supvisors|, among { ``'INITIALIZATION'``, ``'DEPLOYMENT'``,
+                                     ``'OPERATION'``, ``'CONCILIATION'``, ``'RESTARTING'``, ``'SHUTTING_DOWN'``,
+                                     ``'SHUTDOWN'`` }.
+'starting_jobs'    ``list(str)``     The list of |Supvisors| instances having starting jobs in progress.
+'stopping_jobs'    ``list(str)``     The list of |Supvisors| instances having stopping jobs in progress.
+================== ================= ==================
 
 
 |Supvisors| instance status
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-================== ==================
-Key	               Value
-================== ==================
-'identifier'       The deduced name of the |Supvisors| instance.
-'node_name'        The name of the node where the |Supvisors| instance is running.
-'port'             The HTTP port of the |Supvisors| instance.
-'statecode'        The |Supvisors| instance state, in [0;5].
-'statename'        The |Supvisors| instance state as string, among { ``'UNKNOWN'``, ``'CHECKING'``, ``'RUNNING'``
-                   ``'SILENT'``, ``'ISOLATING'``, ``'ISOLATED'`` }.
-'remote_time'      The date of the last ``TICK`` event received from this node, in ms.
-'local_time'       The local date of the last ``TICK`` event received from this node, in ms.
-'loading'          The sum of the expected loading of the processes running on the node, in [0;100]%.
-'sequence_counter' The TICK counter, i.e. the number of Tick events received since it is running.
-================== ==================
+================== ================= ==================
+Key	               Type              Value
+================== ================= ==================
+'identifier'       ``str``           The deduced name of the |Supvisors| instance.
+'node_name'        ``str``           The name of the node where the |Supvisors| instance is running.
+'port'             ``int``           The HTTP port of the |Supvisors| instance.
+'statecode'        ``int``           The |Supvisors| instance state, in [0;5].
+'statename'        ``str``           The |Supvisors| instance state as string, among { ``'UNKNOWN'``, ``'CHECKING'``,
+                                     ``'RUNNING'``, ``'SILENT'``, ``'ISOLATING'``, ``'ISOLATED'`` }.
+'sequence_counter' ``int``           The TICK counter, i.e. the number of Tick events received since it is running.
+'remote_time'      ``float``         The date of the last ``TICK`` event received from this node, in ms.
+'local_time'       ``float``         The local date of the last ``TICK`` event received from this node, in ms.
+'loading'          ``int``           The sum of the expected loading of the processes running on the node, in [0;100]%.
+'process_failure'  ``bool``          True if one of the local processes has crashed or has exited unexpectedly.
+================== ================= ==================
 
 
 Application status
 ~~~~~~~~~~~~~~~~~~
 
-================== ==================
-Key	               Value
-================== ==================
-'application_name' The Application name.
-'statecode'        The Application state, in [0;3].
-'statename'        The Application state as string, among { ``'STOPPED'``, ``'STARTING'``, ``'RUNNING'``,
-                   ``'STOPPING'`` }.
-'major_failure'    True if the application is running and at least one required process is not started.
-'minor_failure'    True if the application is running and at least one optional (not required) process is not started.
-================== ==================
+================== ================= ==================
+Key	               Type              Value
+================== ================= ==================
+'application_name' ``str``           The Application name.
+'statecode'        ``int``           The Application state, in [0;3].
+'statename'        ``str``           The Application state as string, among { ``'STOPPED'``, ``'STARTING'``,
+                                     ``'RUNNING'``, ``'STOPPING'`` }.
+'major_failure'    ``bool``          True if the application is running and at least one required process is not started.
+'minor_failure'    ``bool``          True if the application is running and at least one optional (not required) process
+                                     is not started.
+================== ================= ==================
 
 
 Process status
 ~~~~~~~~~~~~~~
 
-================== ==================
-Key	               Value
-================== ==================
-'application_name' The Application name.
-'process_name'     The Process name.
-'statecode'        The Process state, in {0, 10, 20, 30, 40, 100, 200, 1000}. A special value -1 means that the process
-                   has been deleted as a consequence of an XML-RPC ``update_numprocs``.
-'statename'        The Process state as string, among { ``'STOPPED'``, ``'STARTING'``, ``'RUNNING'``, ``'BACKOFF'``,
-                   ``'STOPPING'``, ``'EXITED'``, ``'FATAL'``, ``'UNKNOWN'`` }. A special value ``DELETED`` means that
-                   the process has been deleted as a consequence of an XML-RPC ``update_numprocs``.
-'expected_exit'    True if the exit status is expected (only when state is ``'EXITED'``).
-'last_event_time'  The date of the last process event received for this process, regardless of the originating
-                   |Supvisors| instance.
-'identifiers'      The deduced names of the |Supvisors| instances where the process is running.
-'extra_args'       The additional arguments passed to the command line of the process.
-================== ==================
+================== ================= ==================
+Key	               Type              Value
+================== ================= ==================
+'application_name' ``str``           The Application name.
+'process_name'     ``str``           The Process name.
+'statecode'        ``int``           The Process state, in {0, 10, 20, 30, 40, 100, 200, 1000}.
+                                     A special value -1 means that the process has been deleted as a consequence
+                                     of an XML-RPC ``update_numprocs``.
+'statename'        ``str``           The Process state as string, among { ``'STOPPED'``, ``'STARTING'``, ``'RUNNING'``,
+                                     ``'BACKOFF'``, ``'STOPPING'``, ``'EXITED'``, ``'FATAL'``, ``'UNKNOWN'`` }.
+                                     A special value ``DELETED`` means that the process has been deleted as a consequence
+                                     of an XML-RPC ``update_numprocs``.
+'expected_exit'    ``bool``          True if the exit status is expected (only when state is ``'EXITED'``).
+'last_event_time'  ``float``         The date of the last process event received for this process, regardless
+                                     of the originating |Supvisors| instance.
+'identifiers'      ``list(str)``     The deduced names of the |Supvisors| instances where the process is running.
+'extra_args'       ``str``           The additional arguments passed to the command line of the process.
+================== ================= ==================
 
 .. hint::
 
@@ -124,33 +124,87 @@ Key	               Value
 Process event
 ~~~~~~~~~~~~~
 
-================== ==================
-Key                Value
-================== ==================
-'group'            The Application name.
-'name'             The Process name.
-'state'            The Process state, in {0, 10, 20, 30, 40, 100, 200, 1000}. A special value -1 means that the process
-                   has been deleted as a consequence of an XML-RPC ``update_numprocs``.
-'expected'         True if the exit status is expected (only when state is 100 - ``EXITED``).
-'now'              The date of the event in the reference time of the node.
-'pid'              The UNIX process ID (only when state is 20 - ``RUNNING`` or 40 - ``STOPPING``).
-'identifier'       The deduced name of the |Supvisors| instance that sent the initial event.
-'extra_args'       The additional arguments passed to the command line of the process.
-'disabled'         True if the process is disabled on the |Supvisors| instance.
-================== ==================
+================== ================= ==================
+Key                Type              Value
+================== ================= ==================
+'group'            ``str``           The Application name.
+'name'             ``str``           The Process name.
+'state'            ``int``           The Process state, in {0, 10, 20, 30, 40, 100, 200, 1000}.
+                                     A special value -1 means that the process has been deleted as a consequence
+                                     of an XML-RPC ``update_numprocs``.
+'expected'         ``bool``          True if the exit status is expected (only when state is 100 - ``EXITED``).
+'now'              ``float``         The date of the event in the reference time of the node.
+'pid'              ``int``           The UNIX process ID (only when state is 20 - ``RUNNING`` or 40 - ``STOPPING``).
+'identifier'       ``str``           The deduced name of the |Supvisors| instance that sent the initial event.
+'extra_args'       ``str``           The additional arguments passed to the command line of the process.
+'disabled'         ``bool``          True if the process is disabled on the |Supvisors| instance.
+================== ================= ==================
+
+
+Host statistics
+~~~~~~~~~~~~~~~
+
+================== ========================= ==================
+Key                Type                      Value
+================== ========================= ==================
+'identifier'       ``str``                   The deduced name of the |Supvisors| instance.
+'target_period'    ``float``                 The configured integration period.
+'period'           ``list(float)``           The start and end uptimes of the integration period, as a list of 2 values.
+'cpu'              ``list(float)``           The CPU (IRIX mode) on the node.
+                                             The first element of the list is the average CPU.
+                                             The following elements correspond to the CPU on every processor core.
+'mem'              ``float``                 The memory occupation on the node.
+'io'               ``dict(str,list(float)``  The Process namespec.
+================== ========================= ==================
+
+
+Process statistics
+~~~~~~~~~~~~~~~~~~
+
+================== ================= ==================
+Key                Type              Value
+================== ================= ==================
+'namespec'         ``str``           The Process namespec.
+'identifier'       ``str``           The deduced name of the |Supvisors| instance.
+'target_period'    ``float``         The configured integration period.
+'period'           ``list(float)``   The start and end uptimes of the integration period, as a list of 2 values.
+'cpu'              ``float``         The CPU (IRIX mode) of the process on the node.
+'mem'              ``float``         The memory occupation of the process on the node.
+================== ================= ==================
+
+
+|ZeroMQ| Implementation
+-----------------------
+
+The |ZeroMQ| implementation relies on a ``PUB-SUB`` pattern provided by |PyZMQ| (:command:`Python` binding of |ZeroMQ|).
+
+When the ``event_link`` option is set to ``ZMQ``, |Supvisors| binds a ``PUBLISH`` |PyZMQ| socket on all interfaces
+using the ``event_port`` option defined in the :ref:`supvisors_section` of the |Supervisor| configuration file.
+
+|Supvisors| publishes the events in multi-parts messages.
+The first part is the message header, as a unicode string. The body follows, encoded in JSON.
+
+To receive the |Supvisors| events, the client application must connect a ``SUBSCRIBE`` |PyZMQ| socket to the address
+defined by the node name and the port number where the |Supvisors| ``PUBLISH`` |PyZMQ| socket is bound.
 
+|PyZMQ| makes it possible to filter the messages received on the client side by subscribing to a part of them.
+To receive all messages, just subscribe using an empty string.
+
+For example, the following :command:`Python` instructions configure the |PyZMQ| socket so as to receive only
+the *Supvisors Status* and *Process Status* events:
 
-Event Clients
--------------
+.. code-block:: python
 
-This section explains how to use receive the |Supvisors| Events from a Python or JAVA client.
+    socket.setsockopt(zmq.SUBSCRIBE, EventHeaders.SUPVISORS.value.encode('utf-8'))
+    socket.setsockopt(zmq.SUBSCRIBE, EventHeaders.PROCESS_STATUS.value.encode('utf-8'))
 
 
 Python Client
 ~~~~~~~~~~~~~
 
-The *SupvisorsZmqEventInterface* is designed to receive the |Supvisors| events from the local |Supvisors| instance.
-It requires PyZmq_ to be installed.
+|Supvisors| provides a :command:`Python` implementation of the |ZeroMQ| client subscriber.
+The *SupvisorsZmqEventInterface* is designed to receive the |Supvisors| events from a |Supvisors| instance.
+It requires |PyZMQ| to be installed.
 
 
 .. automodule:: supvisors.client.zmqsubscriber
@@ -162,13 +216,17 @@ It requires PyZmq_ to be installed.
        .. automethod:: on_application_status(data)
        .. automethod:: on_process_status(data)
        .. automethod:: on_process_event(data)
+       .. automethod:: on_host_statistics(data)
+       .. automethod:: on_process_statistics(data)
 
 .. code-block:: python
 
-    from supvisors.client.zmqsubscriber import *
+    import zmq.asyncio
+    from supvisors.client.clientutils import create_logger
+    from supvisors.client.zmqsubscriber import SupvisorsZmqEventInterface
 
     # create the subscriber thread
-    subscriber = SupvisorsZmqEventInterface(zmq.Context.instance(), port, create_logger())
+    subscriber = SupvisorsZmqEventInterface(zmq.asyncio.Context.instance(), 'localhost', 9003, create_logger())
     # subscribe to all messages
     subscriber.subscribe_all()
     # start the thread
@@ -178,8 +236,8 @@ It requires PyZmq_ to be installed.
 JAVA Client
 ~~~~~~~~~~~
 
-Each |Supvisors| release includes a JAR file that contains a JAVA client.
-It can be downloaded from the `Supvisors releases <https://github.com/julien6387/supvisors/releases>`_.
+A :command:`JAVA` implementation of the |ZeroMQ| client subscriber is made available with each |Supvisors| release
+through a JAR file. This file can be downloaded from the `Supvisors releases <https://github.com/julien6387/supvisors/releases>`_.
 
 The *SupvisorsEventSubscriber* of the ``org.supvisors.event package`` is designed to receive the |Supvisors| events
 from the local |Supvisors| instance.
@@ -205,7 +263,7 @@ The binary JAR of :program:`Google Gson 2.8.6` is available in the
     Context context = ZMQ.context(1);
 
     // create and configure the subscriber
-    SupvisorsEventSubscriber subscriber = new SupvisorsEventSubscriber(60002, context);
+    SupvisorsEventSubscriber subscriber = new SupvisorsEventSubscriber(9003, context);
     subscriber.subscribeToAll();
     subscriber.setListener(new SupvisorsEventListener() {
 
@@ -233,10 +291,84 @@ The binary JAR of :program:`Google Gson 2.8.6` is available in the
         public void onProcessEvent(final SupvisorsProcessEvent event) {
             System.out.println(event);
         }
+
+        @Override
+        public void onHostStatistics(final SupvisorsHostStatistics status) {
+            System.out.println(status);
+        }
+
+        @Override
+        public void onProcessStatistics(final SupvisorsProcessStatistics status) {
+            System.out.println(status);
+        }
     });
 
     // start subscriber in thread
     Thread t = new Thread(subscriber);
     t.start();
 
+
+|websockets| Implementation
+---------------------------
+
+.. attention::
+
+    The |websockets| implementation requires a :command:`Python` version 3.7 or later.
+
+When the ``event_link`` option is set to ``WS``, |Supvisors| creates a |websockets| server that binds on all interfaces
+using the ``event_port`` option defined in the :ref:`supvisors_section` of the |Supervisor| configuration file.
+
+|Supvisors| publishes the event messages as a tuple of header, as a unicode string, and body, encoded in JSON.
+
+To receive the |Supvisors| events, the client application must create a |websockets| client that connects
+to the address defined by the node name and the port number where the |Supvisors| |websockets| server has bound.
+
+Filtering the messages is performed by adding headers to the path of the URI.
+To receive all messages, just add ``all`` to the path of the URI.
+
+For example, the following :command:`Python` instructions configure the |websockets| client so as to receive only
+the *Supvisors Status* and *Process Status* events:
+
+.. code-block:: python
+
+    import websockets
+
+    uri = 'ws://localhost:9003/supvisors/process'
+    async with websockets.connect(uri) as ws:
+        ...
+
+
+Python Client
+~~~~~~~~~~~~~
+
+|Supvisors| provides a :command:`Python` implementation of the |websockets| client.
+The *SupvisorsWsEventInterface* is designed to receive the |Supvisors| events from a |Supvisors| instance.
+It requires |websockets| to be installed.
+
+
+.. automodule:: supvisors.client.wssubscriber
+
+  .. autoclass:: SupvisorsWsEventInterface
+
+       .. automethod:: on_supvisors_status(data)
+       .. automethod:: on_instance_status(data)
+       .. automethod:: on_application_status(data)
+       .. automethod:: on_process_status(data)
+       .. automethod:: on_process_event(data)
+       .. automethod:: on_host_statistics(data)
+       .. automethod:: on_process_statistics(data)
+
+.. code-block:: python
+
+    from supvisors.client.clientutils import create_logger
+    from supvisors.client.wssubscriber import SupvisorsWsEventInterface
+
+    # create the subscriber thread
+    subscriber = SupvisorsWsEventInterface('localhost', 9003, create_logger())
+    # subscribe to all messages
+    subscriber.subscribe_all()
+    # start the thread
+    subscriber.start()
+
+
 .. include:: common.rst
diff --git a/docs/faq.rst b/docs/faq.rst
index 75a1bb1d..e0151ce5 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -49,8 +49,8 @@ of |Supvisors|:
 
 If an ``ImportError`` is raised, here follow some possible causes:
 
-Wrong pip
-~~~~~~~~~
+Wrong pip program
+~~~~~~~~~~~~~~~~~
 
 *Issue:* |Supvisors| may have been installed with a :command:`pip` command corresponding to another :program:`Python`
 version.
diff --git a/docs/images/supvisors_address_host_section.png b/docs/images/supvisors_address_host_section.png
index 9907cca5..c6ab5224 100644
Binary files a/docs/images/supvisors_address_host_section.png and b/docs/images/supvisors_address_host_section.png differ
diff --git a/docs/images/supvisors_address_process_section.png b/docs/images/supvisors_address_process_section.png
index f676d4e5..8211f976 100644
Binary files a/docs/images/supvisors_address_process_section.png and b/docs/images/supvisors_address_process_section.png differ
diff --git a/docs/images/supvisors_application_page.png b/docs/images/supvisors_application_page.png
index 5ddac537..83f5c48f 100644
Binary files a/docs/images/supvisors_application_page.png and b/docs/images/supvisors_application_page.png differ
diff --git a/docs/images/supvisors_conciliation_page.png b/docs/images/supvisors_conciliation_page.png
index 9ce32256..1e4ce130 100644
Binary files a/docs/images/supvisors_conciliation_page.png and b/docs/images/supvisors_conciliation_page.png differ
diff --git a/docs/images/supvisors_gathering.png b/docs/images/supvisors_gathering.png
index 4d72c140..7cf3f651 100644
Binary files a/docs/images/supvisors_gathering.png and b/docs/images/supvisors_gathering.png differ
diff --git a/docs/images/supvisors_main_page.png b/docs/images/supvisors_main_page.png
index efebc326..5a68390b 100644
Binary files a/docs/images/supvisors_main_page.png and b/docs/images/supvisors_main_page.png differ
diff --git a/docs/images/supvisors_menu.png b/docs/images/supvisors_menu.png
index 7b4f018b..a53c8a36 100644
Binary files a/docs/images/supvisors_menu.png and b/docs/images/supvisors_menu.png differ
diff --git a/docs/images/supvisors_scenario_1.png b/docs/images/supvisors_scenario_1.png
index 0c218ad3..6c223636 100644
Binary files a/docs/images/supvisors_scenario_1.png and b/docs/images/supvisors_scenario_1.png differ
diff --git a/docs/images/supvisors_scenario_2.png b/docs/images/supvisors_scenario_2.png
index 63cc9bf9..da530608 100644
Binary files a/docs/images/supvisors_scenario_2.png and b/docs/images/supvisors_scenario_2.png differ
diff --git a/docs/images/supvisors_scenario_3.png b/docs/images/supvisors_scenario_3.png
index d7ad35e3..ba5c92b3 100644
Binary files a/docs/images/supvisors_scenario_3.png and b/docs/images/supvisors_scenario_3.png differ
diff --git a/docs/introduction.rst b/docs/introduction.rst
index 6dff7658..a3b6618c 100644
--- a/docs/introduction.rst
+++ b/docs/introduction.rst
@@ -68,21 +68,23 @@ Installation
 
 |Supvisors| has the following dependencies:
 
-+----------------+------------+-----------------------------------------------------------------+
-| Package        | Release    | Usage                                                           |
-+================+============+=================================================================+
-| |Supervisor|   | 4.2.4      | Base software, extended by |Supvisors|                          |
-+----------------+------------+-----------------------------------------------------------------+
-| PyZMQ_         | 22.0.3     | *:command:`Python` binding of ZeroMQ (optional)*                |
-+----------------+------------+-----------------------------------------------------------------+
-| psutil_        | 5.7.3      | *Information about system usage (optional)*                     |
-+----------------+------------+-----------------------------------------------------------------+
-| matplotlib_    | 3.3.3      | *Graphs for Dashboard (optional)*                               |
-+----------------+------------+-----------------------------------------------------------------+
-| lxml_          | 4.6.2      | *XSD validation of the XML rules file (optional)*               |
-+----------------+------------+-----------------------------------------------------------------+
-| |Flask-RESTX|  | 0.5.1      | *Flask web server with REST API (optional)*                     |
-+----------------+------------+-----------------------------------------------------------------+
++----------------+----------+------------------------------------------------------------------------------------+
+| Package        | Release  | Usage                                                                              |
++================+==========+====================================================================================+
+| |Supervisor|   | 4.2.4    | Base software, extended by |Supvisors|                                             |
++----------------+----------+------------------------------------------------------------------------------------+
+| |psutil|       | 5.7.3    | *Information about system usage (optional)*                                        |
++----------------+----------+------------------------------------------------------------------------------------+
+| matplotlib_    | 3.3.3    | *Graphs for Dashboard (optional)*                                                  |
++----------------+----------+------------------------------------------------------------------------------------+
+| |lxml|         | 4.6.2    | *XSD validation of the XML rules file (optional)*                                  |
++----------------+----------+------------------------------------------------------------------------------------+
+| |Flask-RESTX|  | 0.5.1    | *Expose the Supervisor and Supvisors XML-RPC API through a REST API (optional)*    |
++----------------+----------+------------------------------------------------------------------------------------+
+| |PyZMQ|        | 22.0.3   | *Alternative for the Supvisors Event interface (optional)*                         |
++----------------+----------+------------------------------------------------------------------------------------+
+| |Websockets|   | 10.4     | *Alternative for the Supvisors Event interface (optional, requires Python 3.7+)*   |
++----------------+----------+------------------------------------------------------------------------------------+
 
 With an Internet access
 ~~~~~~~~~~~~~~~~~~~~~~~
@@ -110,6 +112,9 @@ Supvisors can be installed with ``pip install``:
    # install for the ZMQ event interface (includes PyZMQ)
    [bash] > pip install supvisors[zmq]
 
+   # install for the Websockets event interface (includes websockets, requires Python 3.7+)
+   [bash] > pip install supvisors[ws]
+
 Without an Internet access
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -136,6 +141,11 @@ installation has been configured with):
     * :command:`supvisorsflask` provides a |Flask-RESTX| application that exposes the |Supervisor| and |Supvisors|
       XML-RPC APIs through a REST API (refer to the :ref:`rest_api` page).
 
+.. attention::
+
+    :command:`supvisorsflask` uses the |Flask|'s built-in server, which should not be an issue as it is unlikely
+    that this interface ever needs to be scaled.
+
 
 Running |Supvisors|
 -------------------
@@ -158,10 +168,8 @@ However, the |Supervisor| configuration file **MUST**:
 
 .. important::
 
-    A script may be required to start |Supervisor| on several addresses if not configured to run automatically at
-    startup (ssh loop for example).
+    A script may be required to start |Supervisor| on several nodes if not configured to run automatically at
+    startup (:command:`ssh` loop for example).
 
-    It is preferred that all |Supvisors| instances are started within the same (configurable) time frame so that
-    |Supvisors| works as expected. Late starting |Supvisors| instances will yet be considered.
 
 .. include:: common.rst
diff --git a/docs/xml_rpc.rst b/docs/xml_rpc.rst
index 840ff12c..8ce5e5ad 100644
--- a/docs/xml_rpc.rst
+++ b/docs/xml_rpc.rst
@@ -77,6 +77,7 @@ Status
                                          in the local reference time.
             'loading'          ``int``   The sum of the expected loading of the processes running on the |Supvisors|
                                          instance, in [0;100]%.
+            'process_failure'  ``bool``  True if one of the local processes has crashed or has exited unexpectedly.
             'sequence_counter' ``int``   The TICK counter, i.e. the number of Tick events received since it is running.
             'fsm_statecode'    ``int``   The |Supvisors| state as seen by the |Supvisors| instance, in [0;9].
             'fsm_statename'    ``str``   The |Supvisors| state as string, in [``'OFF'``, ``'INITIALIZATION'``,
diff --git a/setup.py b/setup.py
index df76d0c5..fd140610 100644
--- a/setup.py
+++ b/setup.py
@@ -18,20 +18,16 @@
 # ======================================================================
 
 import os
-import sys
 
 from setuptools import setup, find_packages
 
-py_version = sys.version_info[:2]
-if py_version < (3, 6):
-    raise RuntimeError('Supvisors requires Python 3.6 or later')
-
 requires = ['supervisor >= 4.2.4']
 
 statistics_require = ['psutil >= 5.7.3', 'pyparsing >= 2.0.2, < 3', 'matplotlib >= 3.3.3']
 xml_valid_require = ['lxml >= 4.6.2']
 flask_require = ['flask-restx == 0.5.1', 'Werkzeug == 2.0.3']
 zmq_require = ['pyzmq >= 20.0.0']
+websockets_require = ['websockets >= 10.2', 'python_version >= 3.7']
 
 testing_extras = ['pytest >= 2.5.2', 'pytest-cov']
 
@@ -55,6 +51,7 @@
     "Programming Language :: Python :: 3.7",
     "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
     "Topic :: System :: Boot",
     "Topic :: System :: Monitoring",
     "Topic :: System :: Software Distribution"
@@ -77,12 +74,14 @@
           "Rocky 8.5"
       ],
       packages=find_packages(),
+      python_requires='>=3.6',
       install_requires=requires,
       extras_require={'statistics': statistics_require,
                       'xml_valid': xml_valid_require,
                       'flask': flask_require,
                       'zwq': zmq_require,
-                      'all': statistics_require + xml_valid_require + flask_require + zmq_require,
+                      'ws': websockets_require,
+                      'all': statistics_require + xml_valid_require + flask_require + zmq_require + websockets_require,
                       'testing': testing_extras},
       include_package_data=True,
       zip_safe=False,
diff --git a/supvisors/client/clientutils.py b/supvisors/client/clientutils.py
new file mode 100644
index 00000000..f19abb2c
--- /dev/null
+++ b/supvisors/client/clientutils.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# ======================================================================
+# Copyright 2023 Julien LE CLEACH
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ======================================================================
+
+from supervisor import loggers
+from supervisor.loggers import LevelsByName
+
+
+def create_logger(logfile=r'subscriber.log', loglevel=LevelsByName.INFO,
+                  fmt='%(asctime)s;%(levelname)s;%(message)s\n',
+                  rotating=True, maxbytes=10 * 1024 * 1024, backups=1, stdout=True):
+    """ Return a simple Supervisor logger. """
+    logger = loggers.getLogger(loglevel)
+    if stdout:
+        loggers.handle_stdout(logger, fmt)
+    loggers.handle_file(logger, logfile, fmt, rotating, maxbytes, backups)
+    return logger
diff --git a/supvisors/client/java/org/supvisors/common/DataConversion.java b/supvisors/client/java/org/supvisors/common/DataConversion.java
index 136c8891..a93bb5bc 100644
--- a/supvisors/client/java/org/supvisors/common/DataConversion.java
+++ b/supvisors/client/java/org/supvisors/common/DataConversion.java
@@ -69,6 +69,20 @@ public static List<String> arrayToStringList(final Object[] objectArray) {
         return list;
     }
 
+    /**
+     * The arrayToFloatList function creates a list of Float from an array of Object instances.
+     *
+     * @param Object[] objectsArray: The array of Object instances.
+     * @return List<Float>: The list of float results.
+     */
+    public static List<Float> arrayToFloatList(final Object[] objectArray) {
+        List<Float> list = new ArrayList<>(objectArray.length);
+        for (Object object : objectArray) {
+            list.add(Float.parseFloat(Objects.toString(object, null)));
+        }
+        return list;
+    }
+
     /**
      * The namespecToStrings function splits a namespec into group and process names.
      * The function accepts 3 patterns:
diff --git a/supvisors/client/java/org/supvisors/common/StartingStrategy.java b/supvisors/client/java/org/supvisors/common/StartingStrategy.java
index ec44687b..d682a11d 100644
--- a/supvisors/client/java/org/supvisors/common/StartingStrategy.java
+++ b/supvisors/client/java/org/supvisors/common/StartingStrategy.java
@@ -31,7 +31,7 @@ public enum StartingStrategy {
     /** The strategy code. */
     private int strategyCode;
 
-    /** The constructor links the state code to the state name. */
+    /** The constructor links the strategy code to the strategy name. */
     private StartingStrategy(final int strategyCode) {
         this.strategyCode = strategyCode;
     }
diff --git a/supvisors/client/java/org/supvisors/common/SupvisorsHostStatistics.java b/supvisors/client/java/org/supvisors/common/SupvisorsHostStatistics.java
new file mode 100644
index 00000000..9b6d04aa
--- /dev/null
+++ b/supvisors/client/java/org/supvisors/common/SupvisorsHostStatistics.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2023 Julien LE CLEACH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.supvisors.common;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The Class SupvisorsHostStatistics.
+ *
+ * It gives a structured form to the Supvisors Host Statistics information received from the event interface.
+ */
+public class SupvisorsHostStatistics {
+
+    /**
+     * The Class IOBytes.
+     *
+     * It stores the number of received and sent bytes on a given interface.
+     */
+     public class IOBytes {
+
+        /** The number of bytes received on the interface. */
+        private float recvBytes;
+
+        /** The number of bytes sent on the interface. */
+        private float sentBytes;
+
+    /**
+     * The IOBytes constructor.
+     *
+     * @param float recvBytes: The number of received bytes.
+     * @param float sentBytes: The number of sent bytes.
+     */
+     public IOBytes(float recvBytes, float sentBytes) {
+            this.recvBytes = recvBytes;
+            this.sentBytes = sentBytes;
+        }
+    }
+
+    /** The identifier of the Supvisors instance. */
+    private String identifier;
+
+    /** The configured integration period. */
+    private Float target_period;
+
+    /** The actual integration dates. */
+    private List<Float> period;
+
+    /** The CPU on the host during the period. */
+    private List<Float> cpu;
+
+    /** The current Memory occupation on the host. */
+    private Float mem;
+
+    /** The IO statistics. */
+    private HashMap<String, List<Float>> io;
+
+    /**
+     * This constructor gets all information from a HashMap.
+     * NOT USED but kept for future growth.
+     *
+     * @param HashMap statsInfo: The untyped structure got from the XML-RPC.
+     */
+    public SupvisorsHostStatistics(HashMap statsInfo)  {
+        this.identifier = (String) statsInfo.get("identifier");
+        this.target_period = (Float) statsInfo.get("target_period");
+        this.period = DataConversion.arrayToFloatList((Object[]) statsInfo.get("period"));
+        this.cpu = DataConversion.arrayToFloatList((Object[]) statsInfo.get("cpu"));
+        this.mem = (Float) statsInfo.get("memory");
+        this.io = (HashMap<String, List<Float>>) statsInfo.get("io");
+    }
+
+    /**
+     * The getIdentifier method returns the identifier of the Supvisors instance.
+     *
+     * @return String: The identifier of the Supvisors instance.
+     */
+    public String getIdentifier() {
+        return this.identifier;
+    }
+
+    /**
+     * The getTargetPeriod method returns the configured integration period.
+     *
+     * @return Float: The configured integration period.
+     */
+    public Float getTargetPeriod() {
+        return this.target_period;
+    }
+
+    /**
+     * The getStartPeriod method returns the start date of the integrated host statistics.
+     * (end - start) is expected to be greater than the configured integration period.
+     *
+     * @return Float: The start date of the integrated host statistics.
+     */
+    public Float getStartPeriod() {
+        return this.period.get(0);
+    }
+
+    /**
+     * The getEndPeriod method returns the end date of the integrated host statistics.
+     * (end - start) is expected to be greater than the configured integration period.
+     *
+     * @return Float: The node name of the Supvisors instance.
+     */
+    public Float getEndPeriod() {
+        return this.period.get(1);
+    }
+
+    /**
+     * The getAverageCPU method returns the average CPU (IRIX mode) on the host during the integration period.
+     *
+     * @return Float: The average CPU.
+     */
+    public Float getAverageCPU() {
+        return this.cpu.get(0);
+    }
+
+    /**
+     * The getDetailedCPU method returns the CPU (IRIX mode) on all host processor cores during the integration period.
+     *
+     * @return List<Float>: The CPU on every processor cores.
+     */
+    public List<Float> getDetailedCPU() {
+        return this.cpu.subList(1, this.cpu.size() - 1);
+    }
+
+    /**
+     * The getMemory method returns the memory occupation on the host during the integration period.
+     *
+     * @return Float: The memory occupation.
+     */
+    public Float getMemory() {
+        return this.mem;
+    }
+
+    /**
+     * The getIO method returns the number of received and sent bytes on every network interfaces.
+     *
+     * @return HashMap<String, List<Float>: The number of received and sent bytes per network interface.
+     */
+    public HashMap<String, List<Float>> getIO() {
+        return this.io;
+    }
+
+    /**
+     * The getInterfaces method returns the network interfaces names.
+     *
+     * @return Set<String>: The network interfaces names.
+     */
+    Set<String> getInterfaces() {
+        return this.io.keySet();
+    }
+
+    /**
+     * The getInterfaceBytes method returns the number of received and sent bytes on the given network interface.
+     *
+     * @param String: The network interface name among the elements provided by the getInterfaces method.
+     * @return IOBytes: The number of received and sent bytes on the network interface.
+     */
+    IOBytes getInterfaceBytes(final String interfaceName) {
+        List<Float> bytesList = this.io.get(interfaceName);
+        return new IOBytes(bytesList.get(0).floatValue(), bytesList.get(1).floatValue());
+    }
+
+    /**
+     * The toString method returns a printable form of the contents of the SupvisorsHostStatistics instance.
+     *
+     * @return String: The contents of the SupvisorsHostStatistics instance.
+     */
+    public String toString() {
+        return "SupvisorsHostStatistics("
+            + "identifier=" + this.identifier
+            + " target_period=" + this.target_period
+            + " startPeriod=" + this.getStartPeriod()
+            + " endPeriod=" + this.getEndPeriod()
+            + " cpu=" + this.getAverageCPU()
+            + " memory=" + this.getMemory()
+            + " io=" + this.getIO()
+             + ")";
+    }
+
+}
diff --git a/supvisors/client/java/org/supvisors/common/SupvisorsInstanceInfo.java b/supvisors/client/java/org/supvisors/common/SupvisorsInstanceInfo.java
index c2a1a91b..0ec1be4f 100644
--- a/supvisors/client/java/org/supvisors/common/SupvisorsInstanceInfo.java
+++ b/supvisors/client/java/org/supvisors/common/SupvisorsInstanceInfo.java
@@ -28,34 +28,6 @@
  */
 public class SupvisorsInstanceInfo implements SupvisorsAnyInfo {
 
-    /**
-     * The State enumeration for a node.
-     *
-     * UNKNOWN:   used at initialization, before any heartbeat message is received from this Supvisors instance.
-     * CHECKING:  used when the local Supvisors checks the isolation status of this Supvisors instance.
-     * RUNNING:   used when the local Supvisors receives heartbeat messages from this Supvisors instance.
-     * SILENT:    used when the local Supvisors does not receive any heartbeat message from this Supvisors instance.
-     * ISOLATING: used when the local Supvisors is about to disconnect this Supvisors instance.
-     * ISOLATED:  used when the local Supvisors has actually disconnected this Supvisors instance.
-     */
-    public enum State {
-        UNKNOWN(0),
-        CHECKING(1),
-        RUNNING(2),
-        SILENT(3),
-        ISOLATING(4),
-        ISOLATED(5);
-
-        private final int value;
-        public int getValue() {
-            return value;
-        }
-
-        private State(int value) {
-            this.value = value;
-        }
-    }
-
     /** The identifier of the Supvisors instance. */
     private String identifier;
 
@@ -65,8 +37,8 @@ private State(int value) {
     /** The HTTP port of the Supvisors instance. */
     private Integer port;
 
-    /** The node state. */
-    private State statename;
+    /** The instance state. */
+    private SupvisorsInstanceState statename;
 
     /** The date of the last heartbeat message, as received. */
     private Integer remote_time;
@@ -83,6 +55,9 @@ private State(int value) {
      */
     private Integer loading;
 
+    /** True if one of the local processes has crashed or has exited unexpectedly. */
+    private Boolean process_failure;
+
     /**
      * This constructor gets all information from an HashMap.
      *
@@ -92,11 +67,12 @@ public SupvisorsInstanceInfo(HashMap instanceInfo)  {
         this.identifier = (String) instanceInfo.get("identifier");
         this.node_name = (String) instanceInfo.get("node_name");
         this.port = (Integer) instanceInfo.get("port");
-        this.statename = State.valueOf((String) instanceInfo.get("statename"));
+        this.statename = SupvisorsInstanceState.valueOf((String) instanceInfo.get("statename"));
         this.remote_time = (Integer) instanceInfo.get("remote_time");
         this.local_time = (Integer) instanceInfo.get("local_time");
         this.sequence_counter = (Integer) instanceInfo.get("sequence_counter");
         this.loading = (Integer) instanceInfo.get("loading");
+        this.process_failure = (Boolean) instanceInfo.get("process_failure");
     }
 
     /**
@@ -138,9 +114,9 @@ public Integer getPort() {
     /**
      * The getState method returns the state of the node.
      *
-     * @return State: The state of the node.
+     * @return SupvisorsInstanceState: The state of the node.
      */
-    public State getState() {
+    public SupvisorsInstanceState getState() {
         return this.statename;
     }
 
@@ -185,17 +161,30 @@ public Integer getLoading() {
     }
 
     /**
-     * The toString method returns a printable form of the contents of the instance.
+     * The hasProcessFailure method returns True .
+     *
+     * @return State: The loading in percent.
+     */
+    public Boolean hasProcessFailure() {
+        return this.process_failure;
+    }
+
+    /**
+     * The toString method returns True if one of the local processes has crashed or has exited unexpectedly.
      *
-     * @return String: The contents of the instance.
+     * @return Boolean: The process failure status.
      */
     public String toString() {
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         return "SupvisorsInstanceInfo(identifier=" + this.identifier
-            + " node_name=" + this.node_name + " port=" + this.port
-            + " state=" + this.statename + " remoteTime=\"" + sdf.format(new Date(this.remote_time * 1000L)) + "\""
+            + " node_name=" + this.node_name
+            + " port=" + this.port
+            + " state=" + this.statename
+            + " sequenceCounter=" + this.sequence_counter
+            + " remoteTime=\"" + sdf.format(new Date(this.remote_time * 1000L)) + "\""
             + " localTime=\"" + sdf.format(new Date(this.local_time * 1000L)) + "\""
-            + " loading=" + this.loading + " sequenceCounter=" + this.sequence_counter + ")";
+            + " loading=" + this.loading
+            + " processFailure=" + this.process_failure + ")";
     }
 
 }
diff --git a/supvisors/client/java/org/supvisors/common/SupvisorsInstanceState.java b/supvisors/client/java/org/supvisors/common/SupvisorsInstanceState.java
new file mode 100644
index 00000000..186292a8
--- /dev/null
+++ b/supvisors/client/java/org/supvisors/common/SupvisorsInstanceState.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 Julien LE CLEACH
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.supvisors.common;
+
+/**
+ * The SupvisorsInstanceState enumeration.
+ *
+ * UNKNOWN:   used at initialization, before any heartbeat message is received from this Supvisors instance.
+ * CHECKING:  used when the local Supvisors checks the isolation status of this Supvisors instance.
+ * RUNNING:   used when the local Supvisors receives heartbeat messages from this Supvisors instance.
+ * SILENT:    used when the local Supvisors does not receive any heartbeat message from this Supvisors instance.
+ * ISOLATING: used when the local Supvisors is about to disconnect this Supvisors instance.
+ * ISOLATED:  used when the local Supvisors has actually disconnected this Supvisors instance.
+ */
+public enum SupvisorsInstanceState {
+    UNKNOWN(0),
+    CHECKING(1),
+    RUNNING(2),
+    SILENT(3),
+    ISOLATING(4),
+    ISOLATED(5);
+
+    /** The state code. */
+    private final int state;
+
+    /** The constructor links the state code to the state name. */
+    private SupvisorsInstanceState(int state) {
+        this.state = state;
+    }
+}
diff --git a/supvisors/client/java/org/supvisors/common/SupvisorsProcessInfo.java b/supvisors/client/java/org/supvisors/common/SupvisorsProcessInfo.java
index 8b20e9f4..4d5f377d 100644
--- a/supvisors/client/java/org/supvisors/common/SupvisorsProcessInfo.java
+++ b/supvisors/client/java/org/supvisors/common/SupvisorsProcessInfo.java
@@ -19,7 +19,6 @@
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.ArrayList;
 import java.util.List;
 
 
@@ -141,9 +140,9 @@ public String getExtraArgs() {
     }
 
     /**
-     * The toString method returns a printable form of the contents of the instance.
+     * The toString method returns a printable form of the contents of the SupvisorsProcessInfo instance.
      *
-     * @return String: The contents of the instance.
+     * @return String: The contents of the SupvisorsProcessInfo instance.
      */
     public String toString() {
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@@ -154,7 +153,7 @@ public String toString() {
             + " expectedExitStatus=" + this.expected_exit
             + " lastEventTime=\"" + sdf.format(new Date((long) ((double) this.last_event_time * 1000L))) + "\""
             + " identifiers=" + this.identifiers
-            + " extraArgs=" + this.extra_args + ")";
+            + " extraArgs=\"" + this.extra_args + "\")";
     }
 
 }
diff --git a/supvisors/client/java/org/supvisors/common/SupvisorsProcessStatistics.java b/supvisors/client/java/org/supvisors/common/SupvisorsProcessStatistics.java
new file mode 100644
index 00000000..792f0098
--- /dev/null
+++ b/supvisors/client/java/org/supvisors/common/SupvisorsProcessStatistics.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2023 Julien LE CLEACH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.supvisors.common;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * The Class SupvisorsProcessStatistics.
+ *
+ * It gives a structured form to the Supvisors Process Statistics information received from the event interface.
+ */
+public class SupvisorsProcessStatistics {
+
+    /** The namespec of the process. */
+    private String namespec;
+
+    /** The identifier of the Supvisors instance. */
+    private String identifier;
+
+    /** The configured integration period. */
+    private Float target_period;
+
+    /** The actual integration dates. */
+    private List<Float> period;
+
+    /** The CPU of the process during the period. */
+    private Float cpu;
+
+    /** The current Memory occupation of the process on the host. */
+    private Float mem;
+
+    /**
+     * This constructor gets all information from a HashMap.
+     * NOT USED but kept for future growth.
+     *
+     * @param HashMap statsInfo: The untyped structure got from the XML-RPC.
+     */
+    public SupvisorsProcessStatistics(HashMap statsInfo)  {
+        this.namespec = (String) statsInfo.get("namespec");
+        this.identifier = (String) statsInfo.get("identifier");
+        this.target_period = (Float) statsInfo.get("target_period");
+        this.period = DataConversion.arrayToFloatList((Object[]) statsInfo.get("period"));
+        this.cpu = (Float) statsInfo.get("cpu");
+        this.mem = (Float) statsInfo.get("memory");
+    }
+
+    /**
+     * The getNamespec method returns the namespec of the process.
+     *
+     * @return String: The namespec of the process.
+     */
+    public String getNamespec() {
+        return this.namespec;
+    }
+
+    /**
+     * The getIdentifier method returns the identifier of the Supvisors instance.
+     *
+     * @return String: The identifier of the Supvisors instance.
+     */
+    public String getIdentifier() {
+        return this.identifier;
+    }
+
+    /**
+     * The getTargetPeriod method returns the configured integration period.
+     *
+     * @return Float: The configured integration period.
+     */
+    public Float getTargetPeriod() {
+        return this.target_period;
+    }
+
+    /**
+     * The getStartPeriod method returns the start date of the integrated host statistics.
+     * (end - start) is expected to be greater than the configured integration period.
+     *
+     * @return Float: The start date of the integrated host statistics.
+     */
+    public Float getStartPeriod() {
+        return this.period.get(0);
+    }
+
+    /**
+     * The getEndPeriod method returns the end date of the integrated host statistics.
+     * (end - start) is expected to be greater than the configured integration period.
+     *
+     * @return Float: The node name of the Supvisors instance.
+     */
+    public Float getEndPeriod() {
+        return this.period.get(1);
+    }
+
+    /**
+     * The getCPU method returns the CPU (IRIX mode) of the process on the host during the integration period.
+     *
+     * @return Float: The average CPU.
+     */
+    public Float getCPU() {
+        return this.cpu;
+    }
+
+    /**
+     * The getMemory method returns the memory occupation of the process on the host during the integration period.
+     *
+     * @return Float: The memory occupation.
+     */
+    public Float getMemory() {
+        return this.mem;
+    }
+
+    /**
+     * The toString method returns a printable form of the contents of the SupvisorsProcessStatistics instance.
+     *
+     * @return String: The contents of the SupvisorsProcessStatistics instance.
+     */
+    public String toString() {
+        return "SupvisorsProcessStatistics("
+            + "namespec=" + this.namespec
+            + " identifier=" + this.identifier
+            + " target_period=" + this.target_period
+            + " startPeriod=" + this.getStartPeriod()
+            + " endPeriod=" + this.getEndPeriod()
+            + " cpu=" + this.getCPU()
+            + " memory=" + this.getMemory()
+            + ")";
+    }
+
+}
diff --git a/supvisors/client/java/org/supvisors/event/SupvisorsEventListener.java b/supvisors/client/java/org/supvisors/event/SupvisorsEventListener.java
index be7d2426..38f70385 100644
--- a/supvisors/client/java/org/supvisors/event/SupvisorsEventListener.java
+++ b/supvisors/client/java/org/supvisors/event/SupvisorsEventListener.java
@@ -18,8 +18,10 @@
 
 import org.supvisors.common.SupvisorsApplicationInfo;
 import org.supvisors.common.SupvisorsInstanceInfo;
+import org.supvisors.common.SupvisorsHostStatistics;
 import org.supvisors.common.SupvisorsProcessInfo;
 import org.supvisors.common.SupvisorsProcessEvent;
+import org.supvisors.common.SupvisorsProcessStatistics;
 import org.supvisors.common.SupvisorsStatus;
 
 /**
@@ -30,7 +32,7 @@
 public interface SupvisorsEventListener {
 
     /**
-     * The method is called when supvisors subscription is set
+     * The method is called when supvisors status subscription is set
      * and a SupvisorsStatus message is received.
      *
      * @param Supvisors Status: The last SupvisorsStatus received.
@@ -38,7 +40,7 @@ public interface SupvisorsEventListener {
     void onSupvisorsStatus(final SupvisorsStatus status);
 
     /**
-     * The method is called when node subscription is set
+     * The method is called when instance status subscription is set
      * and an Instance Status message is received.
      *
      * @param SupvisorsInstanceInfo: The last SupvisorsInstanceInfo received.
@@ -46,7 +48,7 @@ public interface SupvisorsEventListener {
     void onInstanceStatus(final SupvisorsInstanceInfo status);
 
     /**
-     * The method is called when application subscription is set
+     * The method is called when application status subscription is set
      * and an Application Status message is received.
      *
      * @param SupvisorsApplicationInfo: The last SupvisorsApplicationInfo received.
@@ -54,7 +56,7 @@ public interface SupvisorsEventListener {
     void onApplicationStatus(final SupvisorsApplicationInfo status);
 
     /**
-     * The method is called when application subscription is set
+     * The method is called when process status subscription is set
      * and a Process Status message is received.
      *
      * @param SupvisorsProcessInfo: The last SupvisorsProcessInfo received.
@@ -68,5 +70,20 @@ public interface SupvisorsEventListener {
      * @param SupvisorsProcessEvent: The last SupvisorsProcessEvent received.
      */
     void onProcessEvent(final SupvisorsProcessEvent status);
-}
 
+    /**
+     * The method is called when host statistics subscription is set
+     * and a Host Statistics message is received.
+     *
+     * @param SupvisorsHostStatistics: The last SupvisorsHostStatistics received.
+     */
+    void onHostStatistics(final SupvisorsHostStatistics statistics);
+
+    /**
+     * The method is called when host process subscription is set
+     * and a Process Statistics message is received.
+     *
+     * @param SupvisorsProcessStatistics: The last SupvisorsProcessStatistics received.
+     */
+    void onProcessStatistics(final SupvisorsProcessStatistics statistics);
+}
diff --git a/supvisors/client/java/org/supvisors/event/SupvisorsEventSubscriber.java b/supvisors/client/java/org/supvisors/event/SupvisorsEventSubscriber.java
index c64dd2ae..0d085091 100644
--- a/supvisors/client/java/org/supvisors/event/SupvisorsEventSubscriber.java
+++ b/supvisors/client/java/org/supvisors/event/SupvisorsEventSubscriber.java
@@ -35,6 +35,7 @@
  *
  * The TCP socket is configured with a ZeroMQ SUBSCRIBE pattern.
  * It is connected to the Supvisors instance running on the localhost and bound on the event port.
+ * The message headers are defined in the EventHeaders enumeration of the supvisors.ttypes module.
  */
 public class SupvisorsEventSubscriber implements Runnable {
 
@@ -53,6 +54,12 @@ public class SupvisorsEventSubscriber implements Runnable {
     /** The constant header in ProcessStatus messages. */
     private static final String PROCESS_EVENT_HEADER = "event";
 
+    /** The constant header in HostStatistics messages. */
+    private static final String HOST_STATISTICS_HEADER = "hstats";
+
+    /** The constant header in ProcessStatistics messages. */
+    private static final String PROCESS_STATISTICS_HEADER = "pstats";
+
     /** The ZeroMQ context. */
     private ZContext context;
 
@@ -138,6 +145,20 @@ public void subscribeToProcessEvent() {
         subscribeTo(PROCESS_EVENT_HEADER);
     }
 
+    /**
+     * Subscription to Host statistics.
+     */
+    public void subscribeToHostStatistics() {
+        subscribeTo(HOST_STATISTICS_HEADER);
+    }
+
+    /**
+     * Subscription to Process statistics.
+     */
+    public void subscribeToProcessStatistics() {
+        subscribeTo(PROCESS_STATISTICS_HEADER);
+    }
+
     /**
      * Subscription to event.
      *
@@ -148,47 +169,61 @@ private void subscribeTo(final String header) {
     }
 
     /**
-     * Unubscription from all events.
+     * Unsubscription from all events.
      */
     public void unsubscribeFromAll() {
         this.subscriber.unsubscribe(ZMQ.SUBSCRIPTION_ALL);
     }
 
     /**
-     * Unubscription from Supvisors status events.
+     * Unsubscription from Supvisors status events.
      */
     public void unsubscribeFromSupvisorsStatus() {
         unsubscribeFrom(SUPVISORS_STATUS_HEADER);
     }
 
     /**
-     * Unubscription from Instance status events.
+     * Unsubscription from Instance status events.
      */
     public void unsubscribeFromInstanceStatus() {
         unsubscribeFrom(INSTANCE_STATUS_HEADER);
     }
 
     /**
-     * Unubscription from Application status events.
+     * Unsubscription from Application status events.
      */
     public void unsubscribeFromApplicationStatus() {
         unsubscribeFrom(APPLICATION_STATUS_HEADER);
     }
 
     /**
-     * Unubscription from Process status events.
+     * Unsubscription from Process status events.
      */
     public void unsubscribeFromProcessStatus() {
         unsubscribeFrom(PROCESS_STATUS_HEADER);
     }
 
     /**
-     * Unubscription from Process events.
+     * Unsubscription from Process events.
      */
     public void unsubscribeFromProcessEvent() {
         unsubscribeFrom(PROCESS_EVENT_HEADER);
     }
 
+    /**
+     * Unsubscription from Host statistics.
+     */
+    public void unsubscribeFromHostStatistics() {
+        unsubscribeFrom(HOST_STATISTICS_HEADER);
+    }
+
+    /**
+     * Unsubscription from Process statistics.
+     */
+    public void unsubscribeFromProcessStatistics() {
+        unsubscribeFrom(PROCESS_STATISTICS_HEADER);
+    }
+
     /**
      * Unsubscription from event.
      *
@@ -242,6 +277,12 @@ public void run() {
                     } else if (PROCESS_EVENT_HEADER.equals(header)) {
                         SupvisorsProcessEvent evt = gson.fromJson(body, SupvisorsProcessEvent.class);
                         listener.onProcessEvent(evt);
+                    } else if (HOST_STATISTICS_HEADER.equals(header)) {
+                        SupvisorsHostStatistics stats = gson.fromJson(body, SupvisorsHostStatistics.class);
+                        listener.onHostStatistics(stats);
+                    } else if (PROCESS_STATISTICS_HEADER.equals(header)) {
+                        SupvisorsProcessStatistics stats = gson.fromJson(body, SupvisorsProcessStatistics.class);
+                        listener.onProcessStatistics(stats);
                     }
                 }
             }
@@ -261,8 +302,7 @@ public static void main(String[] args) throws InterruptedException {
         // create ZeroMQ context
         try (ZContext context = new ZContext()) {
             // create and configure the subscriber
-            final SupvisorsEventSubscriber subscriber =
-                new SupvisorsEventSubscriber(60002, context);
+            final SupvisorsEventSubscriber subscriber = new SupvisorsEventSubscriber(60002, context);
             subscriber.subscribeToAll();
             subscriber.setListener(new SupvisorsEventListener() {
 
@@ -290,6 +330,16 @@ public void onProcessStatus(final SupvisorsProcessInfo status) {
                 public void onProcessEvent(final SupvisorsProcessEvent status) {
                     System.out.println(status);
                 }
+
+                @Override
+                public void onHostStatistics(final SupvisorsHostStatistics status) {
+                    System.out.println(status);
+                }
+
+                @Override
+                public void onProcessStatistics(final SupvisorsProcessStatistics status) {
+                    System.out.println(status);
+                }
             });
 
             // start subscriber in thread
@@ -311,4 +361,3 @@ public void run() {
         }
     }
 }
-
diff --git a/supvisors/client/java/org/supvisors/rpc/SupvisorsXmlRpc.java b/supvisors/client/java/org/supvisors/rpc/SupvisorsXmlRpc.java
index 4665c435..ec902e56 100644
--- a/supvisors/client/java/org/supvisors/rpc/SupvisorsXmlRpc.java
+++ b/supvisors/client/java/org/supvisors/rpc/SupvisorsXmlRpc.java
@@ -175,11 +175,9 @@ public HashMap<String, SupvisorsProcessInfo> getProcessInfo(final String namespe
 
     /**
      * The getAllLocalProcessInfo methods returns information about all
-     * processes known to Supervisor, but as a subset of supervisor.getProcessInfo,
-     * including extra arguments.
+     * processes known to Supervisor, but as a subset of supervisor.getProcessInfo, including extra arguments.
      *
-     * @return HashMap<String, SupvisorsProcessEvent>: Information about the
-     * processes, sorted by namespec.
+     * @return HashMap<String, SupvisorsProcessEvent>: Information about the processes, sorted by namespec.
      */
     public HashMap<String, SupvisorsProcessEvent> getAllLocalProcessInfo() throws XmlRpcException {
         Object[] objectsArray = client.rpcCall(Namespace + "get_all_local_process_info", null, Object[].class);
@@ -188,8 +186,7 @@ public HashMap<String, SupvisorsProcessEvent> getAllLocalProcessInfo() throws Xm
 
     /**
      * The getLocalProcessInfo methods returns information about a list of
-     * processes known to Supervisor, but as a subset of supervisor.getProcessInfo,
-     * including extra arguments.
+     * processes known to Supervisor, but as a subset of supervisor.getProcessInfo, including extra arguments.
      *
      * @param String namespec: The name of the process (or "applicationName:processName").
      * @return SupvisorsProcessEvent: Information about the process.
diff --git a/supvisors/client/wssubscriber.py b/supvisors/client/wssubscriber.py
new file mode 100644
index 00000000..9d44faaa
--- /dev/null
+++ b/supvisors/client/wssubscriber.py
@@ -0,0 +1,136 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# ======================================================================
+# Copyright 2023 Julien LE CLEACH
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ======================================================================
+
+from supervisor.loggers import Logger
+
+from supvisors.eventinterface import EventSubscriberInterface
+from supvisors.supvisorswebsocket import WsEventSubscriber
+from supvisors.ttypes import Payload
+
+
+class SupvisorsWsEventInterface(WsEventSubscriber, EventSubscriberInterface):
+    """ The SupvisorsWsEventInterface connects to Supvisors and receives the events published
+    using the Websockets interface.
+
+    The SupvisorsWsEventInterface requires:
+        - the node name where the Supvisors instance is running and publishing its events ;
+        - the event port number used by the Supvisors instance to publish its events ;
+        - a logger.
+
+    Considering the [supvisors] section of the Supervisor configuration file:
+        - the event_link option MUST be set to WS ;
+        - the event_port value MUST be used when creating an instance of this class.
+
+    The default behaviour is to print the messages received.
+    For any other behaviour, just specialize the methods on_xxx_status.
+
+    WARN: Notifications are received in the context of the client thread.
+
+    Example:
+        from supvisors.client.wssubscriber import SupvisorsWsEventInterface
+
+        intf = SupvisorsWsEventInterface('localhost', 9003, logger)
+        intf.subscribe_all()
+        intf.start()
+        # ... receive notifications ...
+        intf.stop()
+    """
+
+    def __init__(self, node_name: str, event_port: int, logger: Logger):
+        """ Initialization of the attributes, declaring self as event subscriber interface. """
+        WsEventSubscriber.__init__(self, self, node_name, event_port, logger)
+
+    def on_supvisors_status(self, data: Payload) -> None:
+        """ Receive and log the contents of the Supvisors Status message.
+
+        :param data: the latest Supvisors status
+        :return: None
+        """
+        self.logger.info(f'SupvisorsWsEventInterface.on_supvisors_status: got Supvisors Status message: {data}')
+
+    def on_instance_status(self, data: Payload) -> None:
+        """ Receive and log the contents of the Supvisors Instance Status message.
+
+        :param data: the latest status about a given Supvisors instance
+        :return: None
+        """
+        self.logger.info(f'SupvisorsWsEventInterface.on_instance_status: got Instance Status message: {data}')
+
+    def on_application_status(self, data: Payload) -> None:
+        """ Receive and log the contents of the Application Status message.
+
+        :param data: the latest status about a given Application
+        :return: None
+        """
+        self.logger.info(f'SupvisorsWsEventInterface.on_application_status: got Application Status message: {data}')
+
+    def on_process_event(self, data: Payload) -> None:
+        """ Receive and log the contents of the Process Event message.
+
+        :param data: the latest event about a given Process
+        :return: None
+        """
+        self.logger.info(f'SupvisorsWsEventInterface.on_process_event: got Process Event message: {data}')
+
+    def on_process_status(self, data: Payload) -> None:
+        """ Receive and log the contents of the Process Status message.
+
+        :param data: the latest status about a given Process
+        :return: None
+        """
+        self.logger.info(f'SupvisorsWsEventInterface.on_process_status: got Process Status message: {data}')
+
+    def on_host_statistics(self, data: Payload) -> None:
+        """ Receive and log the contents of the Host Statistics message.
+
+        :param data: the latest statistics about a given host where Supvisors is running
+        :return: None
+        """
+        self.logger.info(f'SupvisorsWsEventInterface.on_host_statistics: got Host Statistics message: {data}')
+
+    def on_process_statistics(self, data: Payload) -> None:
+        """ Receive and log the contents of the Process Statistics message.
+
+        :param data: the latest statistics about a given process running in Supvisors
+        :return: None
+        """
+        self.logger.info(f'SupvisorsWsEventInterface.on_process_statistics: got Process Statistics message: {data}')
+
+
+if __name__ == '__main__':
+    import argparse
+    import time
+    from supervisor.loggers import LevelsByName
+    from supvisors.client.clientutils import create_logger
+
+    # get arguments
+    parser = argparse.ArgumentParser(description='Start a subscriber to Supvisors events.')
+    parser.add_argument('-n', '--node', type=str, default='127.0.0.1', help='the Supvisors node name')
+    parser.add_argument('-p', '--port', type=int, default=60002, help='the event port of Supvisors')
+    parser.add_argument('-s', '--sleep', type=int, metavar='SEC', default=20,
+                        help='the duration of the subscription')
+    args = parser.parse_args()
+    # create test subscriber
+    ws_intf = SupvisorsWsEventInterface(args.node, args.port, create_logger(loglevel=LevelsByName.INFO))
+    ws_intf.subscribe_all()
+    # start the thread and sleep for a while
+    ws_intf.start()
+    time.sleep(args.sleep)
+    # stop the thread
+    ws_intf.stop()
diff --git a/supvisors/client/zmqsubscriber.py b/supvisors/client/zmqsubscriber.py
index 2fc958f8..b135e9a8 100644
--- a/supvisors/client/zmqsubscriber.py
+++ b/supvisors/client/zmqsubscriber.py
@@ -17,142 +17,124 @@
 # limitations under the License.
 # ======================================================================
 
-import threading
-import zmq
+import zmq.asyncio
+from supervisor.loggers import Logger
 
-from supervisor import loggers
-from supervisor.loggers import LevelsByName
+from supvisors.eventinterface import EventSubscriberInterface
+from supvisors.supvisorszmq import ZmqEventSubscriber
+from supvisors.ttypes import Payload
 
-from supvisors.supvisorszmq import EventSubscriber
-from supvisors.ttypes import EventHeaders
 
+class SupvisorsZmqEventInterface(ZmqEventSubscriber, EventSubscriberInterface):
+    """ The SupvisorsZmqEventInterface connects to Supvisors and receives the events published
+    using the PyZMQ interface.
 
-def create_logger(logfile=r'subscriber.log', loglevel=LevelsByName.INFO,
-                  fmt='%(asctime)s;%(levelname)s;%(message)s\n',
-                  rotating=True, maxbytes=10 * 1024 * 1024, backups=1, stdout=True):
-    """ Return a Supervisor logger. """
-    logger = loggers.getLogger(loglevel)
-    if stdout:
-        loggers.handle_stdout(logger, fmt)
-    loggers.handle_file(logger, logfile, fmt, rotating, maxbytes, backups)
-    return logger
+    The SupvisorsEventInterface requires:
+        - an asynchronous PyZMQ context,
+        - the node name where the Supvisors instance is running and publishing its events,
+        - the event port number used by the Supvisors instance to publish its events,
+        - a logger.
 
+    This event port number MUST correspond to the event_port value set in the [supvisors] section
+    of the Supervisor configuration file.
 
-class SupvisorsZmqEventInterface(threading.Thread):
-    """ The *SupvisorsEventInterface* is a python thread that connects to |Supvisors| and receives the events published.
-    The subscriber attribute shall be used to define the event types of interest.
+    The default behaviour is to print the messages received.
+    For any other behaviour, just specialize the methods on_xxx_status.
 
-    The *SupvisorsEventInterface* requires:
-        - a PyZMQ_ context,
-        - the event port number used by **Supvisors** to publish its events,
-        - a logger reference to log traces.
+    WARN: Notifications are received in the context of the client thread.
 
-    This event port number MUST correspond to the ``event_port`` value set in the ``[supvisors]`` section
-    of the |Supervisor| configuration file.
+    Example:
+        import zmq.asyncio
+        from supvisors.client.zmqsubscriber import SupvisorsZmqEventInterface
 
-    The default behaviour is to print the messages received.
-    For any other behaviour, just specialize the methods `on_xxx_status`.
+        intf = SupvisorsZmqEventInterface(zmq.asyncio.Context.instance(), 'localhost', 9003, logger)
+        intf.subscribe_all()
+        intf.start()
+        # ... receive notifications ...
+        intf.stop()
+    """
 
-    Attributes:
-        - logger: the reference to the logger,
-        - subscriber: the wrapper of the PyZMQ_ socket connected to |Supvisors|,
-        - stop_event: when set, breaks the infinite loop of the thread.
+    def __init__(self, zmq_context: zmq.asyncio.Context, node_name: str, event_port: int, logger: Logger):
+        """ Initialization of the attributes. """
+        ZmqEventSubscriber.__init__(self, zmq_context, self, node_name, event_port, logger)
 
-    Constants:
-        - _Poll_timeout: duration used to time out the PyZMQ_ poller, defaulted to 500 milliseconds.
-    """
+    def on_supvisors_status(self, data: Payload) -> None:
+        """ Receive and log the contents of the Supvisors Status message.
 
-    _Poll_timeout = 500
+        :param data: the latest Supvisors status
+        :return: None
+        """
+        self.logger.info(f'SupvisorsZmqEventInterface.on_supvisors_status: got Supvisors Status message: {data}')
 
-    def __init__(self, zmq_context, event_port, logger):
-        """ Initialization of the attributes. """
-        # thread attributes
-        threading.Thread.__init__(self)
-        # store the parameters
-        self.zmq_context = zmq_context
-        self.event_port = event_port
-        self.logger = logger
-        # create event socket
-        self.subscriber = EventSubscriber(self.zmq_context, self.event_port, self.logger)
-        # create stop event
-        self.stop_event = threading.Event()
-
-    def stop(self):
-        """ This method stops the main loop of the thread. """
-        self.logger.info('SupvisorsEventInterface.stop: request to stop main loop')
-        self.stop_event.set()
-
-    def run(self):
-        """ Main loop of the thread. """
-        # create poller and register event subscriber
-        poller = zmq.Poller()
-        poller.register(self.subscriber.socket, zmq.POLLIN)
-        # poll events every seconds
-        self.logger.info('SupvisorsEventInterface.run: entering main loop')
-        while not self.stop_event.is_set():
-            socks = dict(poller.poll(self._Poll_timeout))
-            # check if something happened on the socket
-            if self.subscriber.socket in socks and socks[self.subscriber.socket] == zmq.POLLIN:
-                self.logger.debug('SupvisorsEventInterface.run: got message on subscriber')
-                try:
-                    message = self.subscriber.receive()
-                except zmq.ZMQError as exc:
-                    self.logger.error(f'SupvisorsEventInterface.run: failed to get data from subscriber: {exc}')
-                else:
-                    try:
-                        event = EventHeaders(message[0])
-                    except ValueError:
-                        self.logger.error(f'SupvisorsEventInterface.run: unexpected event type {message[0]}')
-                    else:
-                        if event == EventHeaders.SUPVISORS:
-                            self.on_supvisors_status(message[1])
-                        elif event == EventHeaders.INSTANCE:
-                            self.on_instance_status(message[1])
-                        elif event == EventHeaders.APPLICATION:
-                            self.on_application_status(message[1])
-                        elif event == EventHeaders.PROCESS_EVENT:
-                            self.on_process_event(message[1])
-                        elif event == EventHeaders.PROCESS_STATUS:
-                            self.on_process_status(message[1])
-        self.logger.warn('SupvisorsEventInterface.run: exiting main loop')
-        self.subscriber.close()
-
-    def on_supvisors_status(self, data):
-        """ Just logs the contents of the |Supvisors| Status message. """
-        self.logger.info(f'SupvisorsEventInterface.on_supvisors_status: got Supvisors Status message: {data}')
-
-    def on_instance_status(self, data):
-        """ Just logs the contents of the Supvisors Instance Status message. """
-        self.logger.info(f'SupvisorsEventInterface.on_instance_status: got Instance Status message: {data}')
-
-    def on_application_status(self, data):
-        """ Just logs the contents of the Application Status message. """
-        self.logger.info(f'SupvisorsEventInterface.on_application_status: got Application Status message: {data}')
-
-    def on_process_event(self, data):
-        """ Just logs the contents of the Process Event message. """
-        self.logger.info(f'SupvisorsEventInterface.on_process_event: got Process Event message: {data}')
-
-    def on_process_status(self, data):
-        """ Just logs the contents of the Process Status message. """
-        self.logger.info(f'SupvisorsEventInterface.on_process_status: got Process Status message: {data}')
+    def on_instance_status(self, data: Payload) -> None:
+        """ Receive and log the contents of the Supvisors Instance Status message.
+
+        :param data: the latest status about a given Supvisors instance
+        :return: None
+        """
+        self.logger.info(f'SupvisorsZmqEventInterface.on_instance_status: got Instance Status message: {data}')
+
+    def on_application_status(self, data: Payload) -> None:
+        """ Receive and log the contents of the Application Status message.
+
+        :param data: the latest status about a given Application
+        :return: None
+        """
+        self.logger.info(f'SupvisorsZmqEventInterface.on_application_status: got Application Status message: {data}')
+
+    def on_process_event(self, data: Payload) -> None:
+        """ Receive and log the contents of the Process Event message.
+
+        :param data: the latest event about a given Process
+        :return: None
+        """
+        self.logger.info(f'SupvisorsZmqEventInterface.on_process_event: got Process Event message: {data}')
+
+    def on_process_status(self, data: Payload) -> None:
+        """ Receive and log the contents of the Process Status message.
+
+        :param data: the latest status about a given Process
+        :return: None
+        """
+        self.logger.info(f'SupvisorsZmqEventInterface.on_process_status: got Process Status message: {data}')
+
+    def on_host_statistics(self, data: Payload) -> None:
+        """ Receive and log the contents of the Host Statistics message.
+
+        :param data: the latest statistics about a given host where Supvisors is running
+        :return: None
+        """
+        self.logger.info(f'SupvisorsZmqEventInterface.on_host_statistics: got Host Statistics message: {data}')
+
+    def on_process_statistics(self, data: Payload) -> None:
+        """ Receive and log the contents of the Process Statistics message.
+
+        :param data: the latest statistics about a given process running in Supvisors
+        :return: None
+        """
+        self.logger.info(f'SupvisorsZmqEventInterface.on_process_statistics: got Process Statistics message: {data}')
 
 
 if __name__ == '__main__':
+    """ Test program. """
     import argparse
     import time
+    from supervisor.loggers import LevelsByName
+    from supvisors.client.clientutils import create_logger
+
     # get arguments
     parser = argparse.ArgumentParser(description='Start a subscriber to Supvisors events.')
-    parser.add_argument('-p', '--port', type=int, default=60002, help='the event port of Supvisors')
-    parser.add_argument('-s', '--sleep', type=int, metavar='SEC', default=10,
+    parser.add_argument('-n', '--node', type=str, default='127.0.0.1', help='the Supvisors node name')
+    parser.add_argument('-p', '--port', type=int, default=60002, help='the Supvisors event port')
+    parser.add_argument('-s', '--sleep', type=int, metavar='SEC', default=20,
                         help='the duration of the subscription')
     args = parser.parse_args()
     # create test subscriber
-    loop = SupvisorsZmqEventInterface(zmq.Context.instance(), args.port, create_logger())
-    loop.subscriber.subscribe_all()
+    zmq_intf = SupvisorsZmqEventInterface(zmq.asyncio.Context.instance(), args.node, args.port,
+                                          create_logger(loglevel=LevelsByName.INFO))
+    zmq_intf.subscribe_all()
     # start thread and sleep for a while
-    loop.start()
+    zmq_intf.start()
     time.sleep(args.sleep)
     # stop thread and halt
-    loop.stop()
-    loop.join()
+    zmq_intf.stop()
diff --git a/supvisors/context.py b/supvisors/context.py
index ddbb4d35..294a7d48 100644
--- a/supvisors/context.py
+++ b/supvisors/context.py
@@ -20,9 +20,9 @@
 from typing import List
 
 from .application import ApplicationRules, ApplicationStatus
+from .eventinterface import EventPublisherInterface
 from .instancestatus import *
 from .process import *
-from .publisherinterface import EventPublisherInterface
 from .ttypes import ApplicationStates, SupvisorsInstanceStates, CLOSING_STATES, NameList, PayloadList, LoadMap
 
 
@@ -139,7 +139,8 @@ def get_nodes_load(self) -> LoadMap:
 
         :return: The nodes load
         """
-        return {ip_address: sum(self.instances[identifier].get_load() for identifier in identifiers)
+        return {ip_address: sum(self.instances[identifier].get_load()
+                                for identifier in identifiers)
                 for ip_address, identifiers in self.supvisors.supvisors_mapper.nodes.items()}
 
     # methods on instances
@@ -636,7 +637,8 @@ def on_process_state_event(self, identifier: str, event: Payload) -> Optional[Pr
                                               ' unknown to local Supervisor')
                     # forced event may be dismissed
                     if updated:
-                        # refresh application status
+                        # refresh internal status
+                        instance_status.update_process(process)
                         application.update_status()
                         # publish process event, status and application status
                         if self.external_publisher:
diff --git a/supvisors/eventinterface.py b/supvisors/eventinterface.py
new file mode 100644
index 00000000..d2d5fd04
--- /dev/null
+++ b/supvisors/eventinterface.py
@@ -0,0 +1,399 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# ======================================================================
+# Copyright 2022 Julien LE CLEACH
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ======================================================================
+
+import asyncio
+from threading import Thread
+from typing import Any, Callable, Optional, Set
+
+from supervisor.loggers import Logger
+
+from .supvisorsmapper import SupvisorsInstanceId
+from .ttypes import EventHeaders, EventLinks, Payload
+
+
+class EventPublisherInterface:
+    """ Interface for the publication of Supvisors events. """
+
+    def close(self) -> None:
+        """ Close the publisher.
+
+        :return: None
+        """
+        raise NotImplementedError
+
+    def send_supvisors_status(self, status: Payload) -> None:
+        """ Send a JSON-serialized supvisors status through the socket.
+
+        :param status: the status to publish
+        :return: None
+        """
+        raise NotImplementedError
+
+    def send_instance_status(self, status: Payload) -> None:
+        """ Send a Supvisors instance status.
+
+        :param status: the status to publish
+        :return: None
+        """
+        raise NotImplementedError
+
+    def send_application_status(self, status: Payload) -> None:
+        """ Send an application status.
+
+        :param status: the status to publish
+        :return: None
+        """
+        raise NotImplementedError
+
+    def send_process_event(self, identifier: str, event: Payload) -> None:
+        """ Send a process event.
+
+        :param identifier: the identifier used to identify the origin of the event
+        :param event: the event to publish
+        :return: None
+        """
+        raise NotImplementedError
+
+    def send_process_status(self, status: Payload) -> None:
+        """ Send a process status.
+
+        :param status: the status to publish
+        :return: None
+        """
+        raise NotImplementedError
+
+    def send_host_statistics(self, statistics: Payload) -> None:
+        """ Send host statistics.
+
+        :param statistics: the statistics to publish
+        :return: None
+        """
+        raise NotImplementedError
+
+    def send_process_statistics(self, statistics: Payload) -> None:
+        """ Send process statistics.
+
+        :param statistics: the statistics to publish
+        :return: None
+        """
+        raise NotImplementedError
+
+
+def create_external_publisher(supvisors: Any) -> EventPublisherInterface:
+    """ Create the relevant event publisher in accordance with the option selected.
+
+    :param supvisors: the global Supvisors instance
+    :return: the event publisher instance
+    """
+    publisher_instance = None
+    publisher_class = None
+    if supvisors.options.event_link == EventLinks.ZMQ:
+        # get a PyZMQ publisher factory
+        try:
+            import zmq
+            from supvisors.supvisorszmq import ZmqEventPublisher
+            publisher_class = ZmqEventPublisher
+        except (ImportError, ModuleNotFoundError):
+            supvisors.logger.error('create_external_publisher: failed to import PyZmq')
+    elif supvisors.options.event_link == EventLinks.WS:
+        # get a Websocket publisher factory
+        try:
+            import websockets
+            from supvisors.supvisorswebsocket import WsEventPublisher
+            publisher_class = WsEventPublisher
+        except (ImportError, ModuleNotFoundError):
+            supvisors.logger.error('create_external_publisher: failed to import websockets')
+    # create the publisher instance
+    if publisher_class:
+        local_instance: SupvisorsInstanceId = supvisors.supvisors_mapper.local_instance
+        publisher_instance = publisher_class(local_instance, supvisors.logger)
+    return publisher_instance
+
+
+class EventSubscriberInterface:
+    """ Interface for the subscription of Supvisors events. """
+
+    def on_supvisors_status(self, data):
+        """ Reception of the Supvisors Status message. """
+        raise NotImplementedError
+
+    def on_instance_status(self, data):
+        """ Reception of the Supvisors Instance Status message. """
+        raise NotImplementedError
+
+    def on_application_status(self, data):
+        """ Reception of the Application Status message. """
+        raise NotImplementedError
+
+    def on_process_event(self, data):
+        """ Reception of the Process Event message. """
+        raise NotImplementedError
+
+    def on_process_status(self, data):
+        """ Reception of the Process Status message. """
+        raise NotImplementedError
+
+    def on_host_statistics(self, data):
+        """ Reception of the Host Statistics message. """
+        raise NotImplementedError
+
+    def on_process_statistics(self, data):
+        """ Reception of the Process Statistics message. """
+        raise NotImplementedError
+
+
+class EventSubscriber:
+    """ Common class providing the event to the implementation of the event subscriber interface. """
+
+    def __init__(self, intf: EventSubscriberInterface, node_name: str, event_port: int, logger: Logger):
+        """ Initialization of the attributes.
+
+        :param intf: the event subscriber interface used to provide the messages received
+        :param node_name: the node name of the Supvisors instance that publishes events
+        :param event_port: the port used by the Supvisors instance to publish events from the node
+        :param logger: the Supvisors logger
+        """
+        self.intf = intf
+        self.node_name: str = node_name
+        self.event_port: int = event_port
+        self.logger: Logger = logger
+        # the subscriptions registered
+        self.headers: Set[str] = set()
+        # create the thread wrapping the Websockets client
+        self.thread: Optional[AsyncEventThread] = None
+        self.reset()
+
+    def reset(self) -> None:
+        """ Create a new thread to handle the event subscriber. """
+        self.thread = AsyncEventThread(self.mainloop, self.node_name, self.event_port)
+
+    async def mainloop(self, stop_evt: asyncio.Event, node_name: str, event_port: int) -> None:
+        """ Infinite loop to be implemented in subclass.
+
+        :return: None
+        """
+        raise NotImplementedError
+
+    def start(self) -> None:
+        """ Start the thread wrapping the event subscriber.
+        Subscriptions must be done before. """
+        self.thread.start()
+
+    def stop(self) -> None:
+        """ Stop and join the thread wrapping the Websocket client.
+
+        :return: None
+        """
+        if self.thread.is_alive():
+            self.thread.stop()
+            self.thread.join()
+
+    def on_receive(self, header, body):
+        """ Called upon the reception of data from the websocket client.
+        This will raise a ValueError if the header is not compliant to the EventHeaders Enum. """
+        event = EventHeaders(header)
+        if event == EventHeaders.SUPVISORS:
+            self.intf.on_supvisors_status(body)
+        elif event == EventHeaders.INSTANCE:
+            self.intf.on_instance_status(body)
+        elif event == EventHeaders.APPLICATION:
+            self.intf.on_application_status(body)
+        elif event == EventHeaders.PROCESS_EVENT:
+            self.intf.on_process_event(body)
+        elif event == EventHeaders.PROCESS_STATUS:
+            self.intf.on_process_status(body)
+        elif event == EventHeaders.HOST_STATISTICS:
+            self.intf.on_host_statistics(body)
+        elif event == EventHeaders.PROCESS_STATISTICS:
+            self.intf.on_process_statistics(body)
+
+    # subscription part
+    def all_subscriptions(self) -> bool:
+        """ Check if all events are subscribed.
+
+        :return: True if all events are subscribed
+        """
+        return all(header.value in self.headers for header in EventHeaders)
+
+    def subscribe_all(self) -> None:
+        """ Subscribe to all event types.
+
+        :return: None
+        """
+        self.headers = {header.value for header in EventHeaders}
+
+    def subscribe(self, header: EventHeaders) -> None:
+        """ Subscribe to an event type.
+
+        :param header: the header to subscribe
+        :return: None
+        """
+        self.headers.add(header.value)
+
+    def subscribe_supvisors_status(self) -> None:
+        """ Subscribe to Supvisors Status messages.
+
+        :return: None
+        """
+        self.subscribe(EventHeaders.SUPVISORS)
+
+    def subscribe_instance_status(self) -> None:
+        """ Subscribe to Supvisors Instance Status messages.
+
+        :return: None
+        """
+        self.subscribe(EventHeaders.INSTANCE)
+
+    def subscribe_application_status(self) -> None:
+        """ Subscribe to Application Status messages.
+
+        :return: None
+        """
+        self.subscribe(EventHeaders.APPLICATION)
+
+    def subscribe_process_event(self) -> None:
+        """ Subscribe to Process Event messages.
+
+        :return: None
+        """
+        self.subscribe(EventHeaders.PROCESS_EVENT)
+
+    def subscribe_process_status(self) -> None:
+        """ Subscribe to Process Status messages.
+
+        :return: None
+        """
+        self.subscribe(EventHeaders.PROCESS_STATUS)
+
+    def subscribe_host_statistics(self) -> None:
+        """ Subscribe to Host Statistics messages.
+
+        :return: None
+        """
+        self.subscribe(EventHeaders.HOST_STATISTICS)
+
+    def subscribe_process_statistics(self) -> None:
+        """ Subscribe to Process Statistics messages.
+
+        :return: None
+        """
+        self.subscribe(EventHeaders.PROCESS_STATISTICS)
+
+    # unsubscription part
+    def unsubscribe_all(self) -> None:
+        """ Unsubscribe from all events.
+
+        :return: None
+        """
+        self.headers = set()
+
+    def unsubscribe(self, header: EventHeaders) -> None:
+        """ Unsubscribe from an event.
+
+        :param header: the header to unsubscribe
+        :return: None
+        """
+        if header.value in self.headers:
+            self.headers.remove(header.value)
+
+    def unsubscribe_supvisors_status(self) -> None:
+        """ Unsubscribe from Supvisors Status messages
+
+        :return: None
+        """
+        self.unsubscribe(EventHeaders.SUPVISORS)
+
+    def unsubscribe_instance_status(self) -> None:
+        """ Unsubscribe from Supvisors Instance Status messages
+
+        :return: None
+        """
+        self.unsubscribe(EventHeaders.INSTANCE)
+
+    def unsubscribe_application_status(self) -> None:
+        """ Unsubscribe from Application Status messages
+
+        :return: None
+        """
+        self.unsubscribe(EventHeaders.APPLICATION)
+
+    def unsubscribe_process_event(self) -> None:
+        """ Unsubscribe from Process Event messages
+
+        :return: None
+        """
+        self.unsubscribe(EventHeaders.PROCESS_EVENT)
+
+    def unsubscribe_process_status(self) -> None:
+        """ Unsubscribe from Process Status messages
+
+        :return: None
+        """
+        self.unsubscribe(EventHeaders.PROCESS_STATUS)
+
+    def unsubscribe_host_statistics(self) -> None:
+        """ Unsubscribe from Host Statistics messages.
+
+        :return: None
+        """
+        self.unsubscribe(EventHeaders.HOST_STATISTICS)
+
+    def unsubscribe_process_statistics(self) -> None:
+        """ Unsubscribe from Process Statistics messages.
+
+        :return: None
+        """
+        self.unsubscribe(EventHeaders.PROCESS_STATISTICS)
+
+
+class AsyncEventThread(Thread):
+    """ Wrapper to start an asynchronous coroutine in a thread. """
+
+    def __init__(self, coro: Callable, node_name: str = '', event_port: int = 9003):
+        """ Initialization of the attributes.
+
+        :param coro: the coroutine to start
+        :param node_name: the node to bind or connect
+        :param event_port: the port to bind or connect
+        """
+        super().__init__(daemon=True)
+        self.coro: Callable = coro
+        self.node_name = node_name
+        self.event_port = event_port
+        # prepare references for the event loop and the stop event
+        self.loop: Optional[asyncio.AbstractEventLoop] = None
+        self.stop_event: Optional[asyncio.Event] = None
+
+    def run(self):
+        """ Start the websocket service or client in a new event loop. """
+        # create the event loop and the stop event
+        self.loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(self.loop)
+        self.stop_event = asyncio.Event()
+        # start the task and wait for its termination
+        self.loop.run_until_complete(self.coro(self.stop_event, self.node_name, self.event_port))
+        self.loop.close()
+
+    def stop(self):
+        """ Stop the websocket service or client. """
+        if self.loop and self.loop.is_running() and self.stop_event and not self.stop_event.is_set():
+            # fire the event within the event loop
+            async def stopit():
+                self.stop_event.set()
+            asyncio.run_coroutine_threadsafe(stopit(), self.loop).result()
diff --git a/supvisors/initializer.py b/supvisors/initializer.py
index 75392a01..85a385c6 100644
--- a/supvisors/initializer.py
+++ b/supvisors/initializer.py
@@ -29,7 +29,7 @@
 from .options import SupvisorsOptions, SupvisorsServerOptions, Automatic, get_logger_configuration
 from .sparser import Parser
 from .statemachine import FiniteStateMachine
-from .statscompiler import StatisticsCompiler
+from .statscompiler import HostStatisticsCompiler, ProcStatisticsCompiler
 from .strategy import RunningFailureHandler
 from .supervisordata import SupervisorData
 from .supvisorsmapper import SupvisorsMapper
@@ -66,7 +66,7 @@ def create_logger(supervisor: Supervisor, logger_config: Payload) -> Logger:
     return logger
 
 
-class Supvisors(object):
+class Supvisors:
     """ The Supvisors class used as a global structure passed to most Supvisors objects. """
 
     # use ';' in logger output as separator as easier to cut
@@ -95,26 +95,40 @@ def __init__(self, supervisor: Supervisor, **config) -> None:
         self.supvisors_mapper = SupvisorsMapper(self)
         try:
             self.supvisors_mapper.configure(self.options.supvisors_list, self.options.core_identifiers)
-        except ValueError as exc:
-            self.logger.critical('Wrong Supvisors configuration (supvisors_list)')
+        except ValueError:
+            self.logger.critical('Supvisors: Wrong Supvisors configuration (supvisors_list)')
             raise
+        # create statistics handlers
+        self.host_collector = None
+        self.process_collector = None
+        try:
+            # test if statistics collector can be created for local host
+            if self.options.host_stats_enabled:
+                from .statscollector import instant_host_statistics
+                self.host_collector = instant_host_statistics
+            if self.options.process_stats_enabled:
+                from .statscollector import ProcessStatisticsCollector
+                self.process_collector = ProcessStatisticsCollector(self.options.collecting_period)
+        except (ImportError, ModuleNotFoundError):
+            self.logger.info('Supvisors: psutil not installed')
+            self.logger.warn('Supvisors: this Supvisors instance cannot not collect statistics')
+        self.host_compiler = HostStatisticsCompiler(self)
+        self.process_compiler = ProcStatisticsCompiler(self.options, self.logger)
         # create context data
         self.context = Context(self)
         # create application starter and stopper
         self.starter = Starter(self)
         self.stopper = Stopper(self)
-        # create statistics handler
-        self.statistician = StatisticsCompiler(self)
         # create the failure handler of crashing processes
         # WARN: must be created before the state machine
         self.failure_handler = RunningFailureHandler(self)
-        # check parsing
+        # check rules files
         try:
             self.parser = Parser(self)
         except Exception as exc:
             self.logger.warn(f'Supvisors: cannot parse rules files: {self.options.rules_files} - {exc}')
             self.parser = None
-        # create event subscriber
+        # create Supervisor event subscriber
         self.listener = SupervisorListener(self)
         # create state machine
         self.fsm = FiniteStateMachine(self)
diff --git a/supvisors/instancestatus.py b/supvisors/instancestatus.py
index 4af32ece..8013456c 100644
--- a/supvisors/instancestatus.py
+++ b/supvisors/instancestatus.py
@@ -25,11 +25,11 @@
 
 from .process import ProcessStatus
 from .supvisorsmapper import SupvisorsInstanceId
-from .ttypes import SupvisorsInstanceStates, SupvisorsStates, InvalidTransition, NamedPidList, Payload
+from .ttypes import SupvisorsInstanceStates, SupvisorsStates, InvalidTransition, Payload
 from .utils import TICK_PERIOD
 
 
-class StateModes(object):
+class StateModes:
 
     def __init__(self, state: SupvisorsStates = SupvisorsStates.OFF, starting_jobs: bool = False,
                  stopping_jobs: bool = False):
@@ -91,7 +91,7 @@ def serial(self):
                 'starting_jobs': self.starting_jobs, 'stopping_jobs': self.stopping_jobs}
 
 
-class SupvisorsInstanceStatus(object):
+class SupvisorsInstanceStatus:
     """ Class defining the status of a Supvisors instance.
 
     Attributes:
@@ -120,6 +120,11 @@ def __init__(self, supvisors_id: SupvisorsInstanceId, supvisors: Any):
         self.processes: Dict[str, ProcessStatus] = {}
         # state and modes
         self.state_modes = StateModes()
+        # the local instance may use the process statistics collector
+        self.process_collector = None
+        is_local = supvisors.supvisors_mapper.local_identifier == self.identifier
+        if is_local and supvisors.options.process_stats_enabled:
+            self.process_collector = supvisors.process_collector
 
     def reset(self):
         """ Reset the contextual part of the Supvisors instance.
@@ -163,13 +168,14 @@ def state(self, new_state):
     def serial(self):
         """ Return a serializable form of the SupvisorsInstanceStatus. """
         payload = {'identifier': self.identifier,
-                   'node_name': self.supvisors_id.host_name,
+                   'node_name': self.supvisors_id.host_id,
                    'port': self.supvisors_id.http_port,
                    'statecode': self.state.value, 'statename': self.state.name,
                    'sequence_counter': self.sequence_counter,
                    'remote_time': capped_int(self.remote_time),
                    'local_time': capped_int(self.local_time),
-                   'loading': self.get_load()}
+                   'loading': self.get_load(),
+                   'process_failure': self.has_error()}
         payload.update(self.state_modes.serial())
         return payload
 
@@ -238,13 +244,28 @@ def check_transition(self, new_state):
         return new_state in self._Transitions[self.state]
 
     # methods on processes
-    def add_process(self, process):
+    def add_process(self, process: ProcessStatus) -> None:
         """ Add a new process to the process list.
 
         :param process: the process status to be added to the Supvisors instance
         :return: None
         """
         self.processes[process.namespec] = process
+        # update the collector withe process if it is already running
+        if self.process_collector:
+            pid = process.get_pid(self.identifier)
+            if pid > 0:
+                self.process_collector.pid_queue.put((process.namespec, pid))
+
+    def update_process(self, process: ProcessStatus) -> None:
+        """ Upon a process state change, check if a pid is available to update the collector.
+
+        :param process: the process status that has been updated
+        :return: None
+        """
+        if self.process_collector:
+            pid = process.get_pid(self.identifier)
+            self.process_collector.pid_queue.put((process.namespec, pid))
 
     def remove_process(self, process: ProcessStatus) -> None:
         """ Remove a process from the process list.
@@ -253,6 +274,9 @@ def remove_process(self, process: ProcessStatus) -> None:
         :return: None
         """
         del self.processes[process.namespec]
+        # update the collector
+        if self.process_collector:
+            self.process_collector.pid_queue.put((process.namespec, 0))
 
     def running_processes(self):
         """ Return the process running on the Supvisors instance.
@@ -260,16 +284,6 @@ def running_processes(self):
         return [process for process in self.processes.values()
                 if process.running_on(self.identifier)]
 
-    def pid_processes(self) -> NamedPidList:
-        """ Return the process running on the Supvisors instance and having a pid.
-       Different from running_processes_on because it excludes the states STARTING and BACKOFF.
-
-        :return: A list of process namespecs and PIDs
-        """
-        return [(process.namespec, process.info_map[self.identifier]['pid'])
-                for process in self.processes.values()
-                if process.pid_running_on(self.identifier)]
-
     def get_load(self) -> int:
         """ Return the load of the Supvisors instance, by summing the declared load of the processes running
         on the Supvisors instance.
@@ -280,6 +294,15 @@ def get_load(self) -> int:
         self.logger.trace(f'SupvisorsInstanceStatus.get_load: Supvisors={self.identifier} load={instance_load}')
         return instance_load
 
+    def has_error(self) -> bool:
+        """ Return True if any process managed by the local Supervisor is in failure.
+
+        :return: the error status
+        """
+        return (self.state == SupvisorsInstanceStates.RUNNING
+                and any(process.crashed(self.identifier)
+                        for process in self.processes.values()))
+
     # dictionary for transitions
     _Transitions = {SupvisorsInstanceStates.UNKNOWN: (SupvisorsInstanceStates.CHECKING,
                                                       SupvisorsInstanceStates.ISOLATING,
diff --git a/supvisors/listener.py b/supvisors/listener.py
index ddde2082..a0462f78 100644
--- a/supvisors/listener.py
+++ b/supvisors/listener.py
@@ -28,9 +28,9 @@
 from supervisor.states import ProcessStates, _process_states_by_code
 from supervisor.xmlrpc import RPCError
 
+from .eventinterface import create_external_publisher, EventPublisherInterface
 from .mainloop import SupvisorsMainLoop
 from .process import ProcessStatus
-from .publisherinterface import create_external_publisher
 from .supvisorssocket import SupvisorsSockets, InternalPublisher
 from .ttypes import (ProcessEvent, ProcessAddedEvent, ProcessRemovedEvent, ProcessEnabledEvent, ProcessDisabledEvent,
                      InternalEventHeaders, RemoteCommEvents, Payload)
@@ -70,14 +70,6 @@ def __init__(self, supvisors: Any):
         """ Initialization of the attributes. """
         self.supvisors = supvisors
         self.logger: Logger = supvisors.logger
-        # test if statistics collector can be created for local host
-        self.collector = None
-        try:
-            from .statscollector import instant_statistics
-            self.collector = instant_statistics
-        except ImportError:
-            self.logger.info('SupervisorListener: psutil not installed')
-            self.logger.warn('SupervisorListener: this Supvisors instance cannot not collect statistics')
         # other attributes
         self.local_identifier: str = supvisors.supvisors_mapper.local_identifier
         self.counter = 0
@@ -85,6 +77,7 @@ def __init__(self, supvisors: Any):
         # Before running, Supervisor forks when daemonized and the sockets are then lost
         self.main_loop: Optional[SupvisorsMainLoop] = None
         self.publisher: Optional[InternalPublisher] = None
+        self.external_publisher: Optional[EventPublisherInterface] = None
         # add new events to Supervisor EventTypes
         add_process_events()
         # subscribe to internal events
@@ -112,6 +105,7 @@ def on_running(self, _):
             # create the Supvisors communication structures
             self.supvisors.sockets = SupvisorsSockets(self.supvisors)
             self.supvisors.external_publisher = create_external_publisher(self.supvisors)
+            self.external_publisher = self.supvisors.external_publisher
             # keep a reference to the internal publisher used to share the events publication
             self.publisher = self.supvisors.sockets.publisher
             # At this point, Supervisor has forked if necessary so the main loop can be started
@@ -121,6 +115,9 @@ def on_running(self, _):
             self.logger.debug('SupervisorListener.on_running: main loop started')
             # Trigger the FSM
             self.supvisors.fsm.next()
+            # Start the process statistics collector
+            if self.supvisors.process_collector:
+                self.supvisors.process_collector.start()
         except Exception:
             # Supvisors shall never endanger the Supervisor thread
             self.logger.critical(f'SupervisorListener.on_running: {format_exc()}')
@@ -130,13 +127,16 @@ def on_stopping(self, _):
         This method stops the Supvisors main loop. """
         self.logger.warn('SupervisorListener.on_stopping: local supervisord is STOPPING')
         try:
+            # Stop the process statistics collector
+            if self.supvisors.process_collector:
+                self.supvisors.process_collector.pid_queue.put(None)
             # force Supervisor to close HTTP servers
             # this will prevent any pending XML-RPC request to block the main loop
             self.supvisors.supervisor_data.close_httpservers()
             # close external publication
-            if self.supvisors.external_publisher:
+            if self.external_publisher:
                 self.logger.debug('SupervisorListener.on_stopping: stopping external publication thread')
-                self.supvisors.external_publisher.close()
+                self.external_publisher.close()
                 self.logger.debug('SupervisorListener.on_stopping: external publication thread stopped')
             # stop the main loop
             self.logger.debug('SupervisorListener.on_stopping: stopping main loop')
@@ -156,6 +156,36 @@ def on_stopping(self, _):
             # Supvisors shall never endanger the Supervisor thread
             self.logger.critical(f'SupervisorListener.on_stopping: {format_exc()}')
 
+    def on_tick(self, event: events.TickEvent) -> None:
+        """ Called when a TickEvent is notified.
+        The event is published to all Supvisors instances.
+        Then statistics are published and periodic task is triggered.
+
+        :param event: the Supervisor TICK event
+        :return: None
+        """
+        try:
+            self.logger.debug(f'SupervisorListener.on_tick: got TickEvent from Supervisor {event}')
+            # reset the time information to get more resolution
+            payload = {'when': time.time(), 'sequence_counter': self.counter}
+            self.counter += 1
+            self.logger.trace(f'SupervisorListener.on_tick: payload={payload}')
+            self.publisher.send_tick_event(payload)
+            # get and publish host statistics at tick time (optional)
+            if self.supvisors.host_collector:
+                stats = self.supvisors.host_collector()
+                self.publisher.send_host_statistics(stats)
+            # get and publish the process statistics collected from the last tick (optional)
+            if self.supvisors.process_collector:
+                stats = []
+                while not self.supvisors.process_collector.stats_queue.empty():
+                    stats.append(self.supvisors.process_collector.stats_queue.get())
+                if stats:
+                    self.publisher.send_process_statistics(stats)
+        except Exception:
+            # Supvisors shall never endanger the Supervisor thread
+            self.logger.critical(f'SupervisorListener.on_tick: {format_exc()}')
+
     def on_process_state(self, event: events.ProcessStateEvent) -> None:
         """ Called when a ProcessStateEvent is sent by the local Supervisor.
         The event is published to all Supvisors instances. """
@@ -176,7 +206,7 @@ def on_process_state(self, event: events.ProcessStateEvent) -> None:
                        'disabled': process_config.disabled}
             self.logger.trace(f'SupervisorListener.on_process_state: payload={payload}')
             self.publisher.send_process_state_event(payload)
-        except Exception as exc:
+        except Exception:
             # Supvisors shall never endanger the Supervisor thread
             self.logger.critical(f'SupervisorListener.on_process_state: {format_exc()}')
 
@@ -186,7 +216,6 @@ def _get_local_process_info(self, namespec: str) -> Payload:
         :param namespec: the process namespec
         :return: the process local information
         """
-        #
         try:
             rpc_intf = self.supvisors.supervisor_data.supvisors_rpc_interface
             return rpc_intf.get_local_process_info(namespec)
@@ -291,30 +320,6 @@ def on_group_removed(self, event: events.ProcessGroupRemovedEvent) -> None:
             # Supvisors shall never endanger the Supervisor thread
             self.logger.critical(f'SupervisorListener.on_group_removed: {format_exc()}')
 
-    def on_tick(self, event: events.TickEvent) -> None:
-        """ Called when a TickEvent is notified.
-        The event is published to all Supvisors instances.
-        Then statistics are published and periodic task is triggered.
-
-        :param event: the Supervisor TICK event
-        :return: None
-        """
-        try:
-            self.logger.debug(f'SupervisorListener.on_tick: got TickEvent from Supervisor {event}')
-            # reset the time information to get more resolution
-            payload = {'when': time.time(), 'sequence_counter': self.counter}
-            self.counter += 1
-            self.logger.trace(f'SupervisorListener.on_tick: payload={payload}')
-            self.publisher.send_tick_event(payload)
-            # get and publish statistics at tick time (optional)
-            if self.collector and self.supvisors.options.stats_enabled:
-                status = self.supvisors.context.instances[self.local_identifier]
-                stats = self.collector(status.pid_processes())
-                self.publisher.send_statistics(stats)
-        except Exception:
-            # Supvisors shall never endanger the Supervisor thread
-            self.logger.critical(f'SupervisorListener.on_tick: {format_exc()}')
-
     def on_remote_event(self, event: events.RemoteCommunicationEvent) -> None:
         """ Called when a RemoteCommunicationEvent is notified.
         This is used to sequence the events received from the Supvisors thread with the other events handled
@@ -358,10 +363,22 @@ def unstack_event(self, message: str):
             self.logger.trace(f'SupervisorListener.unstack_event: got PROCESS_DISABILITY from {event_identifier}:'
                               f' {event_data}')
             self.supvisors.fsm.on_process_disability_event(event_identifier, event_data)
-        elif event_type == InternalEventHeaders.STATISTICS.value:
+        elif event_type == InternalEventHeaders.HOST_STATISTICS.value:
             # this Supvisors could handle statistics even if psutil is not installed
-            self.logger.trace(f'SupervisorListener.unstack_event: got STATISTICS from {event_identifier}: {event_data}')
-            self.supvisors.statistician.push_statistics(event_identifier, event_data)
+            self.logger.trace(f'SupervisorListener.unstack_event: got HOST_STATISTICS from {event_identifier}:'
+                              f' {event_data}')
+            integrated_stats_list = self.supvisors.host_compiler.push_statistics(event_identifier, event_data)
+            if integrated_stats_list and self.external_publisher:
+                for integrated_stats in integrated_stats_list:
+                    self.external_publisher.send_host_statistics(integrated_stats)
+        elif event_type == InternalEventHeaders.PROCESS_STATISTICS.value:
+            # this Supvisors could handle statistics even if psutil is not installed
+            self.logger.trace(f'SupervisorListener.unstack_event: got PROCESS_STATISTICS from {event_identifier}:'
+                              f' {event_data}')
+            integrated_stats_list = self.supvisors.process_compiler.push_statistics(event_identifier, event_data)
+            if integrated_stats_list and self.external_publisher:
+                for integrated_stats in integrated_stats_list:
+                    self.external_publisher.send_process_statistics(integrated_stats)
         elif event_type == InternalEventHeaders.STATE.value:
             self.logger.trace(f'SupervisorListener.unstack_event: got STATE from {event_identifier}')
             self.supvisors.fsm.on_state_event(event_identifier, event_data)
diff --git a/supvisors/mainloop.py b/supvisors/mainloop.py
index b2bf3303..e676a223 100644
--- a/supvisors/mainloop.py
+++ b/supvisors/mainloop.py
@@ -130,7 +130,7 @@ def send_request(self, header: DeferredRequestHeaders, body) -> None:
         # first element of body is always the identifier of the destination Supvisors instance
         identifier = body[0]
         instance = self.supvisors.supvisors_mapper.instances[identifier]
-        self.srv_url.update_url(instance.host_name, instance.http_port)
+        self.srv_url.update_url(instance.host_id, instance.http_port)
         # send message
         if header == DeferredRequestHeaders.CHECK_INSTANCE:
             self.check_instance(*body)
diff --git a/supvisors/options.py b/supvisors/options.py
index 17d2822b..121c8ff8 100644
--- a/supvisors/options.py
+++ b/supvisors/options.py
@@ -22,14 +22,14 @@
 import platform
 from collections import OrderedDict
 from socket import gethostname
-from typing import Dict, List, TypeVar
+from typing import Dict, List, Tuple, TypeVar
 
 from supervisor.datatypes import (Automatic, logfile_name, boolean, integer, byte_size, existing_dirpath,
                                   logging_level, list_of_strings)
 from supervisor.loggers import Logger
 from supervisor.options import expand, ServerOptions, ProcessConfig, FastCGIProcessConfig, EventListenerConfig
 
-from .ttypes import ConciliationStrategies, EventLinks, Payload, StartingStrategies
+from .ttypes import ConciliationStrategies, EventLinks, Payload, StartingStrategies, StatisticsTypes
 
 
 # Options of main section
@@ -68,7 +68,8 @@ class SupvisorsOptions:
         - conciliation_strategy: strategy used to solve conflicts when Supvisors has detected multiple running
           instances of the same program ;
         - starting_strategy: strategy used to start processes on Supvisors instances ;
-        - stats_enable: when False, no statistics will be collected and displayed from this Supvisors instance ;
+        - host_stats_enabled: if False, no host statistics will be collected from this Supvisors instance ;
+        - proc_stats_enabled: if False, no process statistics will be collected from this Supvisors instance ;
         - stats_periods: list of periods for which the statistics will be provided in the Supvisors Web UI ;
         - stats_histo: depth of statistics history ;
         - stats_irix_mode: choice of CPU value display between IRIX and Solaris ;
@@ -113,7 +114,11 @@ def __init__(self, supervisord, logger: Logger, **config):
         self.starting_strategy = self._get_value(config, 'starting_strategy', StartingStrategies.CONFIG,
                                                  self.to_starting_strategy)
         # configure statistics
-        self.stats_enabled = self._get_value(config, 'stats_enabled', True, boolean)
+        # stats_enabled is deprecated
+        stats_enabled = self._get_value(config, 'stats_enabled', (True, True), self.to_statistics_type)
+        self.host_stats_enabled = stats_enabled[0]
+        self.process_stats_enabled = stats_enabled[1]
+        self.collecting_period = self._get_value(config, 'stats_collecting_period', 5, self.to_period)
         self.stats_periods = self._get_value(config, 'stats_periods', [10], self.to_periods)
         self.stats_histo = self._get_value(config, 'stats_histo', 200, self.to_histo)
         self.stats_irix_mode = self._get_value(config, 'stats_irix_mode', False, boolean)
@@ -131,7 +136,9 @@ def __str__(self):
                 f' core_identifiers={self.core_identifiers} disabilities_file={self.disabilities_file}'
                 f' conciliation_strategy={self.conciliation_strategy.name}'
                 f' starting_strategy={self.starting_strategy.name}'
-                f' stats_enabled={self.stats_enabled} stats_periods={self.stats_periods} stats_histo={self.stats_histo}'
+                f' host_stats_enabled={self.host_stats_enabled} process_stats_enabled={self.process_stats_enabled}'
+                f' collecting_period={self.collecting_period}'
+                f' stats_periods={self.stats_periods} stats_histo={self.stats_histo}'
                 f' stats_irix_mode={self.stats_irix_mode}'
                 f' tail_limit={self.tail_limit} tailf_limit={self.tailf_limit}')
 
@@ -185,10 +192,13 @@ def to_port_num(value: str) -> int:
         :param value: the port number as a string
         :return: the port number as an integer
         """
-        value = integer(value)
-        if 0 <= value <= 65535:
-            return value
-        raise ValueError(f'invalid value for port ({value}), expected in [0;65535]')
+        try:
+            port = integer(value)
+            if 1 > port or port > 65535:
+                raise ValueError
+            return port
+        except ValueError:
+            raise ValueError(f'invalid value for port: {value}. integer expected in [1;65535]')
 
     @staticmethod
     def to_timeout(value: str) -> int:
@@ -197,12 +207,15 @@ def to_timeout(value: str) -> int:
         :param value: the timeout as a string
         :return: the timeout as an integer
         """
-        value = integer(value)
-        if SupvisorsOptions.SYNCHRO_TIMEOUT_MIN <= value <= SupvisorsOptions.SYNCHRO_TIMEOUT_MAX:
-            return value
-        raise ValueError(f'invalid value for synchro_timeout: {value}.'
-                         f' expected in [{SupvisorsOptions.SYNCHRO_TIMEOUT_MIN};'
-                         f'{SupvisorsOptions.SYNCHRO_TIMEOUT_MAX}] (seconds)')
+        try:
+            timeout = integer(value)
+            if SupvisorsOptions.SYNCHRO_TIMEOUT_MIN > timeout or timeout > SupvisorsOptions.SYNCHRO_TIMEOUT_MAX:
+                raise ValueError
+            return timeout
+        except ValueError:
+            raise ValueError(f'invalid value for synchro_timeout: {value}.'
+                             f' integer expected in [{SupvisorsOptions.SYNCHRO_TIMEOUT_MIN};'
+                             f'{SupvisorsOptions.SYNCHRO_TIMEOUT_MAX}] (seconds)')
 
     @staticmethod
     def to_ticks(value: str) -> int:
@@ -211,12 +224,15 @@ def to_ticks(value: str) -> int:
         :param value: the number of ticks as a string
         :return: the number of ticks as an integer
         """
-        value = integer(value)
-        if SupvisorsOptions.INACTIVITY_TICKS_MIN <= value <= SupvisorsOptions.INACTIVITY_TICKS_MAX:
-            return value
-        raise ValueError(f'invalid value for inactivity_ticks: {value}.'
-                         f' expected in [{SupvisorsOptions.INACTIVITY_TICKS_MIN};'
-                         f'{SupvisorsOptions.INACTIVITY_TICKS_MAX}]')
+        try:
+            ticks = integer(value)
+            if SupvisorsOptions.INACTIVITY_TICKS_MIN > ticks or ticks > SupvisorsOptions.INACTIVITY_TICKS_MAX:
+                raise ValueError
+            return ticks
+        except ValueError:
+            raise ValueError(f'invalid value for inactivity_ticks: {value}.'
+                             f' integer expected in [{SupvisorsOptions.INACTIVITY_TICKS_MIN};'
+                             f'{SupvisorsOptions.INACTIVITY_TICKS_MAX}]')
 
     @staticmethod
     def to_event_link(value: str) -> EventLinks:
@@ -232,7 +248,7 @@ def to_event_link(value: str) -> EventLinks:
     def to_conciliation_strategy(value: str) -> ConciliationStrategies:
         """ Convert a string into a ConciliationStrategies enum. """
         try:
-            strategy = ConciliationStrategies[value]
+            strategy = ConciliationStrategies[value.upper()]
         except KeyError:
             raise ValueError(f'invalid value for conciliation_strategy: {value}.'
                              f' expected in {[x.name for x in ConciliationStrategies]}')
@@ -242,12 +258,45 @@ def to_conciliation_strategy(value: str) -> ConciliationStrategies:
     def to_starting_strategy(value: str) -> StartingStrategies:
         """ Convert a string into a StartingStrategies enum. """
         try:
-            strategy = StartingStrategies[value]
+            strategy = StartingStrategies[value.upper()]
         except KeyError:
             raise ValueError(f'invalid value for starting_strategy: {value}.'
                              f' expected in {[x.name for x in StartingStrategies]}')
         return strategy
 
+    @staticmethod
+    def to_statistics_type(value: str) -> Tuple[bool, bool]:
+        """ Convert a string into a pair of boolean values to allow host and/or process statistics. """
+        str_stats_types = list_of_strings(value)
+        if len(str_stats_types) == 0:
+            raise ValueError('invalid value for stats_enabled: <empty>')
+        stats_types = []
+        for val in str_stats_types:
+            # first try to use the enumeration values
+            try:
+                stats_types.append(StatisticsTypes[val.upper()])
+            except KeyError:
+                # try the boolean version
+                try:
+                    stats_types.append(StatisticsTypes.ALL if boolean(val) else StatisticsTypes.OFF)
+                except ValueError:
+                    raise ValueError(f'invalid value for stats_enabled: {value}.'
+                                     f' expected in {[x.name for x in StatisticsTypes]}')
+        return (StatisticsTypes.ALL in stats_types or StatisticsTypes.HOST in stats_types,
+                StatisticsTypes.ALL in stats_types or StatisticsTypes.PROCESS in stats_types)
+
+    @staticmethod
+    def to_period(value: str) -> float:
+        """ Convert a string into a list of period values. """
+        try:
+            period = float(value)
+            if 1.0 > period or period > 3600.0:
+                raise ValueError
+            return period
+        except ValueError:
+            raise ValueError(f'invalid value for stats_collecting_period: {value}.'
+                             f' float expected in [1.0;3600.0] (seconds)')
+
     @staticmethod
     def to_periods(value: str) -> List[int]:
         """ Convert a string into a list of period values. """
@@ -259,15 +308,13 @@ def to_periods(value: str) -> List[int]:
         periods = []
         for val in str_periods:
             try:
-                period = integer(val)
-            except ValueError:
-                raise ValueError(f'invalid value for stats_periods: {val}. expected integer')
-            else:
-                if 5 > period or period > 3600:
-                    raise ValueError(f'invalid value for stats_periods: {val}. expected in [5;3600] (seconds)')
-                if period % 5 != 0:
-                    raise ValueError(f'invalid value for stats_periods: {period}. expected multiple of 5')
+                period = float(val)
+                if 1.0 > period or period > 3600.0:
+                    raise ValueError
                 periods.append(period)
+            except ValueError:
+                raise ValueError(f'invalid value for stats_periods: {val}.'
+                                 f' float expected in [1.0;3600.0] (seconds)')
         return sorted(periods)
 
     @staticmethod
@@ -277,10 +324,13 @@ def to_histo(value: str) -> int:
         :param value: the historic size as a string
         :return: the historic size as an integer
         """
-        histo = integer(value)
-        if 10 <= histo <= 1500:
+        try:
+            histo = integer(value)
+            if 10 > histo or histo > 1500:
+                raise ValueError
             return histo
-        raise ValueError(f'invalid value for stats_histo: {value}. expected in [10;1500] (seconds)')
+        except ValueError:
+            raise ValueError(f'invalid value for stats_histo: {value}. integer expected in [10;1500] (seconds)')
 
 
 class SupvisorsServerOptions(ServerOptions):
diff --git a/supvisors/plot.py b/supvisors/plot.py
index 6780c52d..d5b5e1dc 100644
--- a/supvisors/plot.py
+++ b/supvisors/plot.py
@@ -52,7 +52,13 @@ def __init__(self, logger):
         self.ax.tick_params(axis='x', colors=StatisticsPlot.TEXT_COLOR)
         self.ax.tick_params(axis='y', colors=StatisticsPlot.TEXT_COLOR)
         # internal data
-        self.ydata = {}
+        self.xdata = []  # uptime in seconds
+        self.ydata = {}  # variable
+
+    def add_timeline(self, xdata):
+        """ Add the timeline for the abscissa. """
+        if len(xdata) > 0:
+            self.xdata = xdata
 
     def add_plot(self, title, unit, ydata):
         """ Add a defined series of values to the plot. """
@@ -69,24 +75,22 @@ def export_image(self, image_contents):
             plt.ylim(self.get_range(all_ydata))
             # create plots for each series of data
             for i, ((title, unit), ydata) in enumerate(self.ydata.items()):
-                # create X axis
-                xdata = [x for x in range(len(ydata))]
                 # get additional statistics
-                avg, rate, (a, b), dev = get_stats(ydata)
+                avg, rate, (a, b), dev = get_stats(self.xdata, ydata)
                 # plot the data
-                data_line, = self.ax.plot(xdata, ydata, label=title)
+                data_line, = self.ax.plot(self.xdata, ydata, label=title)
                 plot_color = data_line.get_color()
                 # plot the mean line
                 avg_data = [avg, ] * len(ydata)
-                mean_line, = self.ax.plot(xdata, avg_data, label='Mean: {:.2f}{}'.format(avg, unit),
+                mean_line, = self.ax.plot(self.xdata, avg_data, label='Mean: {:.2f}{}'.format(avg, unit),
                                           linestyle='--', color=plot_color)
                 if a is not None:
                     # plot the linear regression
-                    self.ax.plot([xdata[0], xdata[-1]], [a * xdata[0] + b, a * xdata[-1] + b],
+                    self.ax.plot([self.xdata[0], self.xdata[-1]], [a * self.xdata[0] + b, a * self.xdata[-1] + b],
                                  linestyle=':', color=plot_color)
                 if dev is not None:
                     # plot the standard deviation
-                    plt.fill_between(xdata, avg - dev, avg + dev, facecolor=plot_color, alpha=.3)
+                    plt.fill_between(self.xdata, avg - dev, avg + dev, facecolor=plot_color, alpha=.3)
                 # create the legend
                 legend = plt.legend(handles=[data_line, mean_line], loc=i + 1,
                                     fontsize='small', fancybox=True, shadow=True)
@@ -106,8 +110,6 @@ def export_image(self, image_contents):
                 plt.savefig(image_contents.new_image(), bbox_inches='tight', format='png')
             except RuntimeError:
                 self.logger.error('StatisticsPlot.export_image: failed to save the matplotlib figure')
-            # reset yData
-            self.ydata = {}
         # close plot
         plt.close()
 
diff --git a/supvisors/plugin.py b/supvisors/plugin.py
index 69fd01f5..791916fc 100644
--- a/supvisors/plugin.py
+++ b/supvisors/plugin.py
@@ -102,13 +102,7 @@ def update_views() -> None:
     VIEWS['host_io.png'] = {'template': None, 'view': HostNetworkImageView}
 
 
-def make_supvisors_rpcinterface(supervisord: Supervisor, **config) -> RPCInterface:
-    """ Supervisor entry point for Supvisors plugin.
-
-    :param supervisord: the global Supervisor structure
-    :param config: the config attributes read from the Supvisors section
-    :return: the Supvisors XML-RPC interface
-    """
+def apply_patches():
     # update Supervisor Fault definition
     expand_faults()
     # patch the Supervisor ServerOptions.cleanup_fds
@@ -124,7 +118,18 @@ def make_supvisors_rpcinterface(supervisord: Supervisor, **config) -> RPCInterfa
     #    * waiting for Supervisor#1273 to be fixed
     #    * to benefit from the commonalities done in supvisors.ViewHandler
     StatusView.__bases__ = (ViewHandler,)
-    # store the Supvisors instance in supervisord instance to ensure persistence
-    supervisord.supvisors = Supvisors(supervisord, **config)
+
+
+def make_supvisors_rpcinterface(supervisord: Supervisor, **config) -> RPCInterface:
+    """ Supervisor entry point for Supvisors plugin.
+
+    :param supervisord: the global Supervisor structure
+    :param config: the config attributes read from the Supvisors section
+    :return: the Supvisors XML-RPC interface
+    """
+    # store the Supvisors instance within supervisord to ensure persistence and uniqueness
+    if not hasattr(supervisord, 'supvisors'):
+        apply_patches()
+        supervisord.supvisors = Supvisors(supervisord, **config)
     # create and return handler
     return RPCInterface(supervisord.supvisors)
diff --git a/supvisors/process.py b/supvisors/process.py
index f093ad9f..957723b5 100644
--- a/supvisors/process.py
+++ b/supvisors/process.py
@@ -313,6 +313,16 @@ def serial(self) -> Payload:
                 'extra_args': self.extra_args}
 
     # access
+    def get_pid(self, identifier: str) -> int:
+        """ Return the PID of the process located on the Supvisors instance identified.
+
+        :param identifier: the Supvisors instance identifier
+        :return: the PID
+        """
+        if self.state == ProcessStates.RUNNING and identifier in self.running_identifiers:
+            return self.info_map[identifier]['pid']
+        return 0
+
     def disabled(self) -> bool:
         """ Check if the process is disabled on all Supvisors instances knowing the process.
 
@@ -351,20 +361,27 @@ def has_crashed(self) -> bool:
         """
         return any(info['has_crashed'] for info in self.info_map.values())
 
-    def crashed(self) -> bool:
+    def crashed(self, identifier: str = None) -> bool:
         """ Return True if the process has crashed or has exited unexpectedly.
 
         :return: the crash status of the process
         """
-        return ProcessStatus.is_crashed_event(self.state, self.expected_exit)
+        if identifier:
+            info = self.info_map.get(identifier)
+            if not info:
+                return False
+        else:
+            info = {'state': self.state,
+                    'expected': self.expected_exit}
+        return ProcessStatus.is_crashed_event(info)
 
     @staticmethod
-    def is_crashed_event(state: ProcessStates, expected_exit: bool) -> bool:
+    def is_crashed_event(info: Payload) -> bool:
         """ Return True if the process has crashed or has exited unexpectedly.
 
         :return: the crash status of the process
         """
-        return state == ProcessStates.FATAL or (state == ProcessStates.EXITED and not expected_exit)
+        return info['state'] == ProcessStates.FATAL or (info['state'] == ProcessStates.EXITED and not info['expected'])
 
     def stopped(self) -> bool:
         """ Return True if the process is stopped, as designed in Supervisor.
@@ -388,16 +405,6 @@ def running_on(self, identifier: str) -> bool:
         """
         return self.running() and identifier in self.running_identifiers
 
-    def pid_running_on(self, identifier: str) -> bool:
-        """ Check if process is RUNNING on the Supvisors instance identified.
-        Different from running_on as it considers only the RUNNING state and not STARTING or BACKOFF.
-        This is used by the statistics module that requires an existing PID.
-
-        :param identifier: the Supvisors instance identifier
-        :return: the true running status of the process on the considered Supervisor
-        """
-        return self.state == ProcessStates.RUNNING and identifier in self.running_identifiers
-
     def conflicting(self) -> bool:
         """ Check if the process is in a conflicting state (more than one instance running).
 
@@ -476,7 +483,7 @@ def add_info(self, identifier: str, payload: Payload) -> None:
             info['extra_args'] = ''
             self.extra_args = ''
         # keep history that the process has ever crashed in the Supvisors instance
-        has_crashed = ProcessStatus.is_crashed_event(info['state'], info['expected'])
+        has_crashed = ProcessStatus.is_crashed_event(info)
         info['has_crashed'] = has_crashed | info.get('has_crashed', False)
         # log final payload
         self.logger.trace(f'ProcessStatus.add_info: namespec={self.namespec} - payload={info}'
@@ -521,7 +528,7 @@ def update_info(self, identifier: str, payload: Payload, external: bool = True)
         # keep history that the process has ever crashed in the Supvisors instance
         if external:
             # typically, an instance invalidation will not impact the has_crashed status
-            info['has_crashed'] |= ProcessStatus.is_crashed_event(info['state'], info['expected'])
+            info['has_crashed'] |= ProcessStatus.is_crashed_event(info)
         # log final payload
         self.logger.debug(f'ProcessStatus.update_info: namespec={self.namespec} - new info[{identifier}]={info}')
         # process synthetic information
diff --git a/supvisors/publisherinterface.py b/supvisors/publisherinterface.py
deleted file mode 100644
index 60b89465..00000000
--- a/supvisors/publisherinterface.py
+++ /dev/null
@@ -1,94 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# ======================================================================
-# Copyright 2022 Julien LE CLEACH
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-# ======================================================================
-
-from typing import Any
-
-from .supvisorsmapper import SupvisorsInstanceId
-from .ttypes import EventLinks, Payload
-
-
-class EventPublisherInterface:
-    """ Interface for the publication of Supvisors events. """
-
-    def close(self) -> None:
-        """ Close the publisher.
-
-        :return: None
-        """
-        raise NotImplementedError
-
-    def send_supvisors_status(self, status: Payload) -> None:
-        """ Send a JSON-serialized supvisors status through the socket.
-
-        :param status: the status to publish
-        :return: None
-        """
-        raise NotImplementedError
-
-    def send_instance_status(self, status: Payload) -> None:
-        """ Send a Supvisors instance status.
-
-        :param status: the status to publish
-        :return: None
-        """
-        raise NotImplementedError
-
-    def send_application_status(self, status: Payload) -> None:
-        """ Send an application status.
-
-        :param status: the status to publish
-        :return: None
-        """
-        raise NotImplementedError
-
-    def send_process_event(self, identifier: str, event: Payload) -> None:
-        """ Send a process event.
-
-        :param identifier: the identifier used to identify the origin of the event
-        :param event: the event to publish
-        :return: None
-        """
-        raise NotImplementedError
-
-    def send_process_status(self, status: Payload) -> None:
-        """ Send a process status.
-
-        :param status: the status to publish
-        :return: None
-        """
-        raise NotImplementedError
-
-
-def create_external_publisher(supvisors: Any) -> EventPublisherInterface:
-    """ Create the relevant event publisher in accordance with the option selected.
-
-    :param supvisors: the global Supvisors instance
-    :return: the event publisher instance
-    """
-    publisher_instance = None
-    if supvisors.options.event_link == EventLinks.ZMQ:
-        # Create a PyZMQ publisher
-        local_instance: SupvisorsInstanceId = supvisors.supvisors_mapper.local_instance
-        try:
-            import zmq
-            from supvisors.supvisorszmq import EventPublisher
-            publisher_instance = EventPublisher(local_instance, supvisors.logger)
-        except (ImportError, ModuleNotFoundError):
-            supvisors.logger.error('create_external_publisher: failed to import PyZmq')
-    return publisher_instance
diff --git a/supvisors/rpcinterface.py b/supvisors/rpcinterface.py
index ae71bcba..15d66482 100644
--- a/supvisors/rpcinterface.py
+++ b/supvisors/rpcinterface.py
@@ -517,7 +517,7 @@ def onwait() -> RPCInterface.OnWaitReturnType:
 
     def start_any_process(self, strategy: EnumParameterType, regex: str, extra_args: str = '',
                           wait: bool = True) -> WaitStringReturnType:
-        """ Start a process named ``namespec`` iaw the strategy and the rules file.
+        """ Start one process among those matching the ``regex`` iaw the strategy and the rules file.
         WARN: the 'wait_exit' rule is not considered here.
 
         :param StartingStrategies strategy: the strategy used to choose a **Supvisors** instance,
diff --git a/supvisors/statscollector.py b/supvisors/statscollector.py
index 4e1f1d07..e82c40c2 100644
--- a/supvisors/statscollector.py
+++ b/supvisors/statscollector.py
@@ -17,24 +17,37 @@
 # limitations under the License.
 # ======================================================================
 
+import multiprocessing as mp
 import os
-from time import time
-from typing import List
+from time import sleep, time
+from typing import Dict, List
 
-from psutil import cpu_times, net_io_counters, virtual_memory, Process, NoSuchProcess
+import psutil
 
-from .ttypes import JiffiesList, InterfaceInstantStats, InstantStatistics, NamedPidList, ProcessStats
+from .ttypes import Jiffies, JiffiesList, InterfaceInstantStats, ProcessStats
 from .utils import mean
 
+# Default sleep time when nothing to do
+SLEEP_TIME = 0.2
 
 # CPU statistics
-def instant_cpu_statistics() -> JiffiesList:
+def instant_cpu_statistics() -> Jiffies:
+    """ Return the instant average work+idle jiffies. """
+    cpu_stat = psutil.cpu_times()
+    work = (cpu_stat.user + cpu_stat.nice + cpu_stat.system
+            + cpu_stat.irq + cpu_stat.softirq
+            + cpu_stat.steal + cpu_stat.guest)
+    idle = (cpu_stat.idle + cpu_stat.iowait)
+    return work, idle
+
+
+def instant_all_cpu_statistics() -> JiffiesList:
     """ Return the instant work+idle jiffies for all the processors.
     The average on all processors is inserted in front of the list. """
     work: List[float] = []
     idle: List[float] = []
     # CPU details
-    cpu_stats = cpu_times(percpu=True)
+    cpu_stats = psutil.cpu_times(percpu=True)
     for cpu_stat in cpu_stats:
         work.append(cpu_stat.user + cpu_stat.nice + cpu_stat.system
                     + cpu_stat.irq + cpu_stat.softirq
@@ -51,7 +64,7 @@ def instant_memory_statistics() -> float:
     """ Return the instant value of the memory reserved.
     This is different from the memory used as it does not include the percent of memory that is available
     (in cache or swap). """
-    return virtual_memory().percent
+    return psutil.virtual_memory().percent
 
 
 # Network statistics
@@ -59,44 +72,187 @@ def instant_io_statistics() -> InterfaceInstantStats:
     """ Return the instant values of receive / sent bytes per network interface. """
     result: InterfaceInstantStats = {}
     # IO details
-    io_stats = net_io_counters(pernic=True)
+    io_stats = psutil.net_io_counters(pernic=True)
     for intf, io_stat in io_stats.items():
         result[intf] = io_stat.bytes_recv, io_stat.bytes_sent
     return result
 
 
+# Host statistics
+def instant_host_statistics():
+    """ Get a snapshot of some host resources. """
+    return {'now': time(),
+            'cpu': instant_all_cpu_statistics(),
+            'mem': instant_memory_statistics(),
+            'io': instant_io_statistics()}
+
+
 # Process statistics
-def instant_process_statistics(pid, children=True) -> ProcessStats:
+process_attributes = ['cpu_times', 'memory_percent']
+
+def instant_process_statistics(proc: psutil.Process, get_children=True) -> ProcessStats:
     """ Return the instant jiffies and memory values for the process identified by pid. """
-    work = memory = 0
     try:
-        # get main process
-        proc = Process(pid)
-        # Note: using Process.oneshot has no value (2 accesses and memory_percent not included in cache for Linux)
+        # get main process statistics
+        proc_stats = proc.as_dict(attrs=process_attributes)
         # WARN: children CPU times are not considered fully recursively, so exclude children CPU times and iowait
-        work = sum(proc.cpu_times()[:2])
-        memory = proc.memory_percent()
+        work = sum(proc_stats['cpu_times'][:2])
+        memory = proc_stats['memory_percent']
         # consider all children recursively
-        if children:
+        if get_children:
             for p in proc.children(recursive=True):
-                memory += sum(p.cpu_times()[:2])
-                memory += p.memory_percent()
-    except (NoSuchProcess, ValueError):
+                try:
+                    p_stats = p.as_dict(attrs=process_attributes)
+                    work += sum(p_stats['cpu_times'][:2])
+                    memory += p_stats['memory_percent']
+                except (psutil.NoSuchProcess, psutil.AccessDenied, ValueError):
+                    # process may have disappeared in the interval
+                    pass
+        # take into account the number of processes for the process work
+        return work, memory
+    except (psutil.NoSuchProcess, psutil.AccessDenied, ValueError):
         # process may have disappeared in the interval
-        pass
-    # take into account the number of processes for the process work
-    return work, memory
-
-
-# Snapshot of all resources
-def instant_statistics(named_pid_list: NamedPidList) -> InstantStatistics:
-    """ Return a tuple of all measures taken on the CPU, Memory and IO resources. """
-    proc_statistics = {process_name: (pid, instant_process_statistics(pid))
-                       for process_name, pid in named_pid_list}
-    # add supervisord statistics, without considering its children
-    # supervisord is the father of all named_pids so that should give a grand total
-    self_pid = os.getpid()
-    proc_statistics['supervisord'] = self_pid, instant_process_statistics(self_pid, False)
-    # return a Tuple with everything
-    return (time(), instant_cpu_statistics(), instant_memory_statistics(),
-            instant_io_statistics(), proc_statistics)
+        return None
+
+
+class CollectedProcesses:
+    """ Class holding the psutil structures for all the processes to collect statistics from. """
+
+    def __init__(self, stats_queue: mp.Queue, period: float, supervisor_pid: int):
+        """ Initialization of the attributes. """
+        self.stats_queue: mp.Queue = stats_queue
+        self.period: float = period
+        self.processes: List[Dict] = []
+        # additional information obout the number of CPU cores, used for the Solaris mode
+        self.nb_cores = mp.cpu_count()
+        # define the supervisor process and this collector process
+        self.supervisor_process: Dict = {'last': 0,
+                                         'supervisor': psutil.Process(supervisor_pid),
+                                         'collector': psutil.Process()}
+
+    def update_process_list(self, namespec: str, pid: int):
+        """ Update the list of processes to be collected, considering that events may have been missed. """
+        # check if namespec is already being collected
+        found = next(((idx, proc_stats) for idx, proc_stats in enumerate(self.processes)
+                      if proc_stats['namespec'] == namespec), None)
+        if found:
+            # process already in list
+            idx, proc_stats = found
+            if proc_stats['process'].pid != pid:
+                # process has been stopped in the gap. remove the entry
+                self.processes.pop(idx)
+                # publish the information that it has been stopped
+                self.stats_queue.put({'namespec': namespec,
+                                      'pid': 0,
+                                      'now': time()})
+                if pid > 0:
+                    # new process running. mark as not found so that it is re-inserted
+                    found = False
+        if not found:
+            # add new entry in list so that it is processed in priority
+            try:
+                process = psutil.Process(pid)
+                self.processes.append({'namespec': namespec, 'last': 0, 'process': process})
+            except psutil.NoSuchProcess:
+                # process has been stopped in the gap
+                # no need to post the termination as the process died before anything has been sent
+                pass
+
+    def collect_supervisor(self):
+        """ Collect supervisor and collector statistics, without considering any other supervisor children. """
+        current_time = time()
+        elapsed = current_time - self.supervisor_process['last']
+        if elapsed >= self.period:
+            supervisor = self.supervisor_process['supervisor']
+            sup_stats = instant_process_statistics(supervisor, False)
+            collector = self.supervisor_process['collector']
+            col_stats = instant_process_statistics(collector, False)
+            if sup_stats and col_stats:  # the contrary is really not meant to happen
+                # push the results into the stats_queue
+                self.stats_queue.put({'namespec': 'supervisord',
+                                      'pid': supervisor.pid,
+                                      'now': current_time,
+                                      'cpu': instant_cpu_statistics(),  # CPU needed for process CPU calculation
+                                      'nb_cores': self.nb_cores,  # needed for Solaris mode
+                                      'proc_work': sup_stats[0] + col_stats[0],
+                                      'proc_memory': sup_stats[1] + col_stats[1]})
+                # re-insert the process in front of the list
+                self.supervisor_process['last'] = current_time
+
+    def collect_recent_process(self) -> bool:
+        """ Collect the next process statistics. """
+        if len(self.processes):
+            current_time = time()
+            elapsed = current_time - self.processes[-1]['last']
+            if elapsed >= self.period:
+                # period has passed. get statistics on the process
+                proc_data = self.processes.pop()
+                proc = proc_data['process']
+                proc_stats = instant_process_statistics(proc)
+                if proc_stats:
+                    # push the results into the stats_queue
+                    self.stats_queue.put({'namespec': proc_data['namespec'],
+                                          'pid': proc.pid,
+                                          'now': current_time,
+                                          'cpu': instant_cpu_statistics(),  # CPU needed for process CPU calculation
+                                          'proc_work': proc_stats[0],
+                                          'proc_memory': proc_stats[1]})
+                    # update date and re-insert the process in front of the list
+                    proc_data['last'] = current_time
+                    self.processes.insert(0, proc_data)
+                else:
+                    # warn the subscribers that the process has been stopped
+                    self.stats_queue.put({'namespec': proc_data['namespec'],
+                                          'pid': 0,
+                                          'now': current_time})
+                    # not to be re-inserted in the list
+                # no need to wait. ready to process another process
+                return True
+        # most ready process is not ready enough. wait
+        return False
+
+
+def collect_process_statistics(pid_queue: mp.Queue, stats_queue: mp.Queue, period: int, supervisor_pid: int):
+    """ Collector main loop. """
+    collector = CollectedProcesses(stats_queue, period, supervisor_pid)
+    # loop forever
+    stopped = False
+    while not stopped:
+        # get new pids to collect statistics from
+        while not pid_queue.empty():
+            message = pid_queue.get()
+            if message is None:
+                stopped = True
+            else:
+                collector.update_process_list(*message)
+        if not stopped:
+            # test if supervisord is ready to be collected
+            collector.collect_supervisor()
+            # test if the last element of the list is ready to be collected
+            ready = collector.collect_recent_process()
+            # if no process is ready to be collected, go to sleep
+            if not ready:
+                sleep(SLEEP_TIME)
+
+
+class ProcessStatisticsCollector:
+    """ Collector of instant process statistics.
+    The psutil processes are cached to speed up. """
+
+    def __init__(self, period: int):
+        # store the period
+        self.period = period
+        # create communication queues
+        self.pid_queue = mp.SimpleQueue()  # used to receive pids and program names
+        self.stats_queue = mp.SimpleQueue()  # used to send process statistics
+        # the process attribute
+        self.process = None
+
+    def start(self):
+        """ Create and start the collector process.
+        The process must be started by the process that has created it, hence not created in the constructor
+        because Supervisor may fork depending on the options selected."""
+        self.process = mp.Process(target=collect_process_statistics,
+                                  args=(self.pid_queue, self.stats_queue, self.period, os.getpid()),
+                                  daemon=True)
+        self.process.start()
diff --git a/supvisors/statscompiler.py b/supvisors/statscompiler.py
index 23958a44..9a80fe83 100644
--- a/supvisors/statscompiler.py
+++ b/supvisors/statscompiler.py
@@ -19,21 +19,24 @@
 
 from typing import Dict, List, Optional, Tuple
 
-from .ttypes import (CPUHistoryStats, CPUInstantStats, InstantStatistics, InterfaceHistoryStats, InterfaceInstantStats,
-                     InterfaceIntegratedStats, JiffiesList, MemHistoryStats, ProcessHistoryStatsMap)
+from .ttypes import (CPUHistoryStats, CPUInstantStats, JiffiesList, Jiffies,
+                     InterfaceHistoryStats, InterfaceInstantStats, InterfaceIntegratedStats,
+                     MemHistoryStats, TimesHistoryStats,
+                     ProcessCPUHistoryStats, ProcessMemHistoryStats,
+                     Payload, PayloadList)
 
 # additional annotation types
 FloatList = List[float]
-ProcessIntegratedStatsMap = Dict[Tuple[str, int], Tuple[float, float]]  # {(namespec, pid): (cpu, mem)}
-IntegratedStats = Tuple[float, CPUInstantStats, float, InterfaceIntegratedStats, ProcessIntegratedStatsMap]
+IntegratedProcessStats = Tuple[float, float, float] # cpu, mem, seconds
+IntegratedHostStatistics = Tuple[float, CPUInstantStats, float, InterfaceIntegratedStats]
 
 
 # CPU statistics
-def cpu_statistics(last: JiffiesList, ref: JiffiesList) -> CPUInstantStats:
+def cpu_statistics(last_values: JiffiesList, ref_values: JiffiesList) -> CPUInstantStats:
     """ Return the CPU loading for all the processors between last and ref measures.
     The average on all processors is inserted in front of the list. """
     cpu = []
-    for unit in zip(last, ref):
+    for unit in zip(last_values, ref_values):
         work = unit[0][0] - unit[1][0]
         idle = unit[0][1] - unit[1][1]
         total = work + idle
@@ -41,20 +44,21 @@ def cpu_statistics(last: JiffiesList, ref: JiffiesList) -> CPUInstantStats:
     return cpu
 
 
-def cpu_total_work(last: JiffiesList, ref: JiffiesList) -> float:
+def cpu_total_work(last_values: Jiffies, ref_values: Jiffies) -> float:
     """ Return the total work on the average CPU between last and ref measures. """
-    work = last[0][0] - ref[0][0]
-    idle = last[0][1] - ref[0][1]
+    work = last_values[0] - ref_values[0]
+    idle = last_values[1] - ref_values[1]
     return work + idle
 
 
 # Network statistics
-def io_statistics(last: InterfaceInstantStats, ref: InterfaceInstantStats, duration: float) -> InterfaceIntegratedStats:
+def io_statistics(last_values: InterfaceInstantStats, ref_values: InterfaceInstantStats,
+                  duration: float) -> InterfaceIntegratedStats:
     """ Return the rate of receive / sent bytes per second per network interface. """
     io_stats = {}
-    for intf, (last_recv, last_sent) in last.items():
-        if intf in ref.keys():
-            ref_recv, ref_sent = ref[intf]
+    for intf, (last_recv, last_sent) in last_values.items():
+        if intf in ref_values.keys():
+            ref_recv, ref_sent = ref_values[intf]
             # Warning taken from psutil documentation (https://pythonhosted.org/psutil/#network)
             # on some systems such as Linux, on a very busy or long-lived system these numbers may wrap
             # (restart from zero), see issues #802. Applications should be prepared to deal with that.
@@ -66,83 +70,48 @@ def io_statistics(last: InterfaceInstantStats, ref: InterfaceInstantStats, durat
     return io_stats
 
 
-# Process statistics
-def cpu_process_statistics(last: float, ref: float, total_work: float) -> float:
-    """ Return the CPU loading of the process between last and ref measures. """
-    # process may have been started between ref and last
-    return 100.0 * (last - ref) / total_work
-
-
-# Calculate resources taken between two snapshots
-def statistics(last: InstantStatistics, ref: InstantStatistics) -> IntegratedStats:
-    """ Return resources' statistics from two series of measures. """
-    # for use in client display
-    duration = last[0] - ref[0]
-    cpu = cpu_statistics(last[1], ref[1])
-    mem = last[2]
-    io = io_statistics(last[3], ref[3], duration)
-    # process statistics
-    work = cpu_total_work(last[1], ref[1])
-    proc = {}
-    # when tuples are deserialized through JSON, they become lists
-    for process_name, last_pid_stats in last[4].items():
-        # find same process in ref
-        ref_pid_stats = ref[4].get(process_name, None)
-        # calculate cpu if ref is found
-        # pid must be identical (in case of process restart in the interval)
-        if ref_pid_stats and last_pid_stats[0] == ref_pid_stats[0]:
-            # need the work jiffies in the interval
-            proc_cpu = cpu_process_statistics(last_pid_stats[1][0], ref_pid_stats[1][0], work)
-            proc[process_name, last_pid_stats[0]] = proc_cpu, last_pid_stats[1][1]
-    return last[0], cpu, mem, io, proc
+# remove first data of list if size exceeds depth
+def trunc_depth(lst, depth):
+    """ Ensure that the list does not exceed the maximum historic size. """
+    while len(lst) > depth:
+        lst.pop(0)
 
 
 # Class for statistics storage
-class StatisticsInstance(object):
+class HostStatisticsInstance:
     """ This class handles resources statistics for a given node and period. """
 
-    def __init__(self, period, depth, logger):
-        """ Initialization of the attributes.
-        As period is a multiple of 5 and a call to pushStatistics is expected every 5 seconds,
-        period is used as a simple counter. """
+    def __init__(self, identifier: str, period: float, depth: float, logger):
+        """ Initialization of the attributes. """
         # keep reference to the logger
         self.logger = logger
         # parameters
-        self.period: int = period // 5
-        self.depth: int = depth
-        self.counter: int = -1
-        self.ref_stats: Optional[InstantStatistics] = None
+        self.identifier: str = identifier
+        self.period: float = period
+        self.depth: float = depth
+        self.ref_stats: Payload = {}
+        self.ref_start_time: float = 0.0
         # data structures
+        self.times: TimesHistoryStats = []
         self.cpu: CPUHistoryStats = []
         self.mem: MemHistoryStats = []
         self.io: InterfaceHistoryStats = {}
-        self.proc: ProcessHistoryStatsMap = {}
 
-    def clear(self):
-        """ Reset all attributes. """
-        self.counter = -1
-        self.ref_stats = None
-        # data structures
-        self.cpu = []
-        self.mem = []
-        self.io = {}
-        self.proc = {}
-
-    def find_process_stats(self, namespec):
-        """ Return the process statistics related to the namespec. """
-        return next((stats for (process_name, pid), stats in self.proc.items()
-                     if process_name == namespec), None)
+    def _push_times_stats(self, time_value: float):
+        """ Add new Times statistics. """
+        self.times.append(time_value)
+        trunc_depth(self.times, self.depth)
 
     def _push_cpu_stats(self, cpu_stats: FloatList):
         """ Add new CPU statistics. """
         for lst in self.cpu:
             lst.append(cpu_stats.pop(0))
-            self.trunc_depth(lst)
+            trunc_depth(lst, self.depth)
 
-    def _push_mem_stats(self, mem_stats: float):
+    def _push_mem_stats(self, mem_value: float):
         """ Add new MEM statistics. """
-        self.mem.append(mem_stats)
-        self.trunc_depth(self.mem)
+        self.mem.append(mem_value)
+        trunc_depth(self.mem, self.depth)
 
     def _push_io_stats(self, io_stats: InterfaceIntegratedStats):
         """ Add new IO statistics. """
@@ -159,8 +128,8 @@ def _push_io_stats(self, io_stats: InterfaceIntegratedStats):
                 recv_stats.append(recv_bytes)
                 sent_stats.append(sent_bytes)
                 # remove too old values when max depth is reached
-                self.trunc_depth(recv_stats)
-                self.trunc_depth(sent_stats)
+                trunc_depth(recv_stats, self.depth)
+                trunc_depth(sent_stats, self.depth)
         # destroy obsolete elements
         for intf in destroy_list:
             self.logger.warn(f'StatisticsInstance.push_io_stats: obsolete interface: {intf}')
@@ -170,81 +139,265 @@ def _push_io_stats(self, io_stats: InterfaceIntegratedStats):
             self.logger.warn(f'StatisticsInstance.push_io_stats: new interface: {intf}')
             self.io[intf] = [recv_bytes], [sent_bytes]
 
-    def _push_process_stats(self, proc_stats):
-        """ Add new process statistics. """
-        # as process list is dynamic, there are special rules
-        destroy_list = []
-        for named_pid, (cpu_stats, mem_stats) in self.proc.items():
-            new_values = proc_stats.pop(named_pid, None)
-            if new_values is None:
-                # element is obsolete
-                destroy_list.append(named_pid)
-            else:
-                cpu_value, mem_value = new_values
-                cpu_stats.append(cpu_value)
-                mem_stats.append(mem_value)
-                # remove too old values when max depth is reached
-                self.trunc_depth(cpu_stats)
-                self.trunc_depth(mem_stats)
-        # destroy obsolete elements
-        for named_pid in destroy_list:
-            del self.proc[named_pid]
-        # add new elements
-        for named_pid, (cpu_value, mem_value) in proc_stats.items():
-            self.proc[named_pid] = [cpu_value], [mem_value]
-
-    def push_statistics(self, stats: InstantStatistics):
-        """ Calculates new statistics given a new series of measures. """
-        self.counter += 1
-        if self.counter % self.period == 0:
-            if self.ref_stats:
+    def integrate(self, last: Payload) -> IntegratedHostStatistics:
+        """ Return resources' statistics from two series of measures. """
+        # for use in client display
+        duration: float = last['now'] - self.ref_stats['now']
+        cpu: CPUInstantStats = cpu_statistics(last['cpu'], self.ref_stats['cpu'])
+        mem: float = last['mem']
+        io: InterfaceIntegratedStats = io_statistics(last['io'], self.ref_stats['io'], duration)
+        return last['now'] - self.ref_start_time, cpu, mem, io
+
+    def push_statistics(self, stats: Payload) -> Payload:
+        """ Calculate new statistics given a new series of measures. """
+        result = {}
+        if self.ref_stats:
+            if stats['now'] - self.ref_stats['now'] >= self.period:
                 # rearrange data so that there is less processing afterwards
-                integ_stats = statistics(stats, self.ref_stats)
+                uptime, cpu, mem, io = self.integrate(stats)
+                # create the result structure (use a copy as the _push functions will pop)
+                result = {'identifier': self.identifier,
+                          'target_period': self.period,
+                          'period': (self.ref_stats['now'] - self.ref_start_time,
+                                     stats['now'] - self.ref_start_time),
+                          'cpu': cpu.copy(),
+                          'mem': mem,
+                          'io': io.copy()}
                 # add new values
-                self._push_cpu_stats(integ_stats[1])
-                self._push_mem_stats(integ_stats[2])
-                self._push_io_stats(integ_stats[3])
-                # add new Process CPU / Mem values to Process list
-                self._push_process_stats(integ_stats[4])
-            else:
-                # init some data structures
-                self.cpu = [[] for _ in stats[1]]
-                self.io = {intf: ([], []) for intf in stats[3]}
+                self._push_times_stats(uptime)
+                self._push_cpu_stats(cpu)
+                self._push_mem_stats(mem)
+                self._push_io_stats(io)
+                # new stats become the reference stats for next integration
+                self.ref_stats = stats
+        else:
+            # new stats are the reference stats on first occurrence
             self.ref_stats = stats
-
-    # remove first data of all lists if size exceeds depth
-    def trunc_depth(self, lst):
-        """ Ensure that the list does not exceed the maximum historic size. """
-        while len(lst) > self.depth:
-            lst.pop(0)
+            self.ref_start_time = stats['now']
+            # init some data structures
+            self.cpu = [[] for _ in stats['cpu']]
+            self.io = {intf: ([], []) for intf in stats['io']}
+        return result
 
 
 # Class used to compile statistics coming from all instances
-class StatisticsCompiler(object):
-    """ This class handles stores statistics for all instances and periods.
+class HostStatisticsCompiler:
+    """ This class stores host statistics for all instances and periods.
 
     Attributes are:
 
-        - data: a dictionary containing a StatisticsInstance entry for each pair of node and period,
-        - nbcores: a dictionary giving the number of processor cores per node.
+        - instance_map: a dictionary containing a HostStatisticsInstance entry for each pair of node and period,
+        - nb_cores: a dictionary giving the number of processor cores per node.
         """
 
+    # additional annotation type for internal purpose
+    HostStatisticsMap = Dict[str, Dict[float, HostStatisticsInstance]]
+
     def __init__(self, supvisors):
         """ Initialization of the attributes. """
-        self.data = {identifier: {period: StatisticsInstance(period, supvisors.options.stats_histo, supvisors.logger)
-                                  for period in supvisors.options.stats_periods}
-                     for identifier in supvisors.supvisors_mapper.instances}
-        self.nbcores: Dict[str, int] = {identifier: 1 for identifier in supvisors.supvisors_mapper.instances}
+        # the list of Supvisors instances is fixed so prepare the structures
+        self.instance_map: HostStatisticsCompiler.HostStatisticsMap = {
+            identifier: {period: HostStatisticsInstance(identifier, period, supvisors.options.stats_histo,
+                                                        supvisors.logger)
+                         for period in supvisors.options.stats_periods}
+            for identifier in supvisors.supvisors_mapper.instances}
+        self.nb_cores: Dict[str, int] = {}
+
+    def get_stats(self, identifier: str, period: int) -> HostStatisticsInstance:
+        """ Return the HostStatisticsInstance corresponding to a Supvisors instance and a period. """
+        identifier_instance = self.instance_map.get(identifier)
+        if identifier_instance:
+            return identifier_instance.get(period)
+
+    def get_nb_cores(self, identifier: str) -> int:
+        """ Return the number of CPU cores linked to a Supvisors instance. """
+        return self.nb_cores.get(identifier, 0)
+
+    def push_statistics(self, identifier: str, stats: Payload) -> PayloadList:
+        """ Insert a new statistics measure for identifier. """
+        integrated_stats = []
+        if identifier in self.instance_map:
+            for period, stats_instance in self.instance_map[identifier].items():
+                result = stats_instance.push_statistics(stats)
+                if result:
+                    integrated_stats.append(result)
+            # set the number of processor cores
+            # in the case of multiple cores, the first series of values are average values
+            nb = len(stats['cpu'])
+            self.nb_cores[identifier] = nb if nb == 1 else nb - 1
+        return integrated_stats
 
-    def clear(self, identifier: str):
-        """ For a given node, clear the StatisticsInstance for all periods. """
-        for period in self.data[identifier].values():
-            period.clear()
 
-    def push_statistics(self, identifier: str, stats: InstantStatistics):
-        """ Insert a new statistics measure for identifier. """
-        for stats_instance in self.data[identifier].values():
-            stats_instance.push_statistics(stats)
-        # set the number of processor cores
-        nb = len(stats[1])
-        self.nbcores[identifier] = nb if nb == 1 else nb - 1
+# Process statistics
+def cpu_process_statistics(last: float, ref: float, total_work: float) -> float:
+    """ Return the CPU loading of the process between last and ref measures. """
+    # process may have been started between ref and last
+    return 100.0 * (last - ref) / total_work
+
+
+class ProcStatisticsInstance:
+    """ This class handles statistics for a process running on a Supervisor instance and for a given period. """
+
+    def __init__(self, namespec: str = '', identifier: str = '', period: int = 0, depth: int = 0):
+        """ Initialization of the attributes. """
+        # parameters
+        self.namespec: str = namespec
+        self.identifier: str = identifier
+        self.period: int = period
+        self.depth: int = depth
+        self.ref_stats: Payload = {}
+        self.ref_start_time: float = 0.0
+        # data structures
+        self.times: TimesHistoryStats = []
+        self.cpu: ProcessCPUHistoryStats = []
+        self.mem: ProcessMemHistoryStats = []
+
+    def integrate(self, last: Payload) -> IntegratedProcessStats:
+        """ Return resources' statistics from two series of measures. """
+        # CPU statistics
+        work = cpu_total_work(last['cpu'], self.ref_stats['cpu'])
+        # calculate cpu if ref is found
+        # need the work jiffies in the interval
+        proc_cpu = cpu_process_statistics(last['proc_work'], self.ref_stats['proc_work'], work)
+        return proc_cpu, last['proc_memory'], last['now'] - self.ref_start_time
+
+    def push_statistics(self, proc_stats: Payload) -> Payload:
+        """ Calculate new statistics given a new series of measures. """
+        result = {}
+        if self.ref_stats:
+            if proc_stats['now'] - self.ref_stats['now'] >= self.period:
+                # rearrange data so that there is less processing afterwards
+                cpu_value, mem_value, time_value = self.integrate(proc_stats)
+                # create the result structure (use a copy as the _push functions will pop)
+                result = {'namespec': self.namespec,
+                          'identifier': self.identifier,
+                          'target_period': self.period,
+                          'period': (self.ref_stats['now'] - self.ref_start_time,
+                                     proc_stats['now'] - self.ref_start_time),
+                          'cpu': cpu_value,
+                          'mem': mem_value}
+                # add new Process CPU / Mem values to Process list
+                self.cpu.append(cpu_value)
+                self.mem.append(mem_value)
+                self.times.append(time_value)
+                # remove too old values when max depth is reached
+                trunc_depth(self.cpu, self.depth)
+                trunc_depth(self.mem, self.depth)
+                trunc_depth(self.times, self.depth)
+                # new stats become the reference stats for next integration
+                self.ref_stats = proc_stats
+        else:
+            # new stats are the reference stats on first occurrence
+            self.ref_stats = proc_stats
+            self.ref_start_time = proc_stats['now']
+        return result
+
+
+class ProcStatisticsHolder:
+    """ This class stores process statistics for a process that may be running on all Supervisor instances.
+
+    Attributes are:
+
+        - namespec: the process namespec
+        - pid: the process PID
+        - options: the Supvisors options
+        - logger: the global Supvisors logger
+        - instance_map: a dictionary of ProcStatisticsInstance for all Supvisors instances where the process is running
+        and for all periods.
+    """
+
+    IdentifierInstanceMap = Dict[str, Tuple[int, Dict[int, ProcStatisticsInstance]]]
+
+    def __init__(self, namespec: str, options, logger):
+        """ Initialization of the attributes. """
+        self.namespec = namespec
+        self.options = options
+        self.logger = logger
+        # {identifier: (pid, {period: ProcStatisticsInstance}}
+        self.instance_map: ProcStatisticsHolder.IdentifierInstanceMap = {}
+
+    def get_stats(self, identifier: str, period: int) -> Optional[ProcStatisticsInstance]:
+        """ Return the ProcStatisticsInstance corresponding to a Supvisors instance and a period. """
+        _, identifier_instance = self.instance_map.get(identifier, (0, None))
+        if identifier_instance:
+            return identifier_instance.get(period)
+
+    def push_statistics(self, identifier: str, process_stats: Payload) -> PayloadList:
+        """ Consider a new list of process statistics received from a Supvisors instance.
+        Stopped processes (pid=0) are not considered if there is no existing holder. """
+        integrated_stats = []
+        pid = process_stats['pid']
+        if pid == 0:
+            # process has been stopped on Supervisord instance
+            self.logger.debug(f'ProcStatisticsHolder.push_statistics: {self.namespec} stopped on {identifier}')
+            self.instance_map.pop(identifier, None)
+        else:
+            ref_pid, identifier_instance = self.instance_map.get(identifier, (0, None))
+            if not identifier_instance or pid != ref_pid:
+                # process has been (re-)started on Supervisord instance
+                self.logger.debug(f'ProcStatisticsHolder.push_statistics: {self.namespec} started on {identifier}')
+                identifier_instance = {period: ProcStatisticsInstance(self.namespec, identifier,
+                                                                      period, self.options.stats_histo)
+                                       for period in self.options.stats_periods}
+                self.instance_map[identifier] = pid, identifier_instance
+            # update process statistics for all periods
+            for period_instance in identifier_instance.values():
+                result = period_instance.push_statistics(process_stats)
+                if result:
+                    integrated_stats.append(result)
+        return integrated_stats
+
+
+class ProcStatisticsCompiler:
+    """ This class stores process statistics for all processes running on all Supervisor instances.
+
+    Attributes are:
+
+        - options: the Supvisors options
+        - logger: the global Supvisors logger
+        - holder_map: a dictionary of ProcessesStatisticsHolder.
+    """
+
+    def __init__(self, options, logger):
+        """ Initialization of the attributes. """
+        self.options = options
+        self.logger = logger
+        self.holder_map: Dict[str, ProcStatisticsHolder] = {}
+        # keep a CPU core map per identifier for Solaris mode
+        self.nb_cores = {}
+
+    def get_stats(self, namespec: str, identifier: str, period: int) -> Optional[ProcStatisticsInstance]:
+        """ Return the ProcStatisticsInstance corresponding to a namespec, a Supvisors instance and a period. """
+        proc_holder = self.holder_map.get(namespec)
+        if proc_holder:
+            return proc_holder.get_stats(identifier, period)
+
+    def get_nb_cores(self, identifier: str) -> int:
+        """ Return the number of CPU cores linked to a Supvisors instance. """
+        return self.nb_cores.get(identifier, 0)
+
+    def push_statistics(self, identifier: str, stats: PayloadList) -> PayloadList:
+        """ Consider a new list of process statistics received from a Supvisors instance.
+        Stopped processes (pid=0) are not considered if there is no existing holder. """
+        integrated_stats = []
+        for process_stats in stats:
+            namespec = process_stats['namespec']
+            pid = process_stats['pid']
+            proc_holder = self.holder_map.get(namespec)
+            if pid > 0 and not proc_holder:
+                # new process spawned
+                self.holder_map[namespec] = proc_holder = ProcStatisticsHolder(namespec, self.options, self.logger)
+                self.logger.debug(f'ProcStatisticsCompiler.push_statistics: holder created for {namespec}')
+            if proc_holder:
+                # update the holder data
+                result = proc_holder.push_statistics(identifier, process_stats)
+                if result:
+                    integrated_stats.extend(result)
+                # if no more data, delete the holder
+                if not proc_holder.instance_map:
+                    self.logger.debug(f'ProcStatisticsCompiler.push_statistics: holder deleted for {namespec}')
+                    del self.holder_map[namespec]
+            # set the number of processor cores on the identifier if provided (only in supervisord stats)
+            if 'nb_cores' in process_stats:
+                self.nb_cores[identifier] = process_stats['nb_cores']
+        return integrated_stats
diff --git a/supvisors/strategy.py b/supvisors/strategy.py
index cac8d7ac..a06f51ba 100644
--- a/supvisors/strategy.py
+++ b/supvisors/strategy.py
@@ -338,7 +338,7 @@ def get_node(supvisors: Any, strategy: StartingStrategies, identifiers: NameList
     identifier = get_supvisors_instance(supvisors, strategy, identifiers, expected_load)
     # get the corresponding host name
     if identifier:
-        return supvisors.supvisors_mapper.instances[identifier].host_name
+        return supvisors.supvisors_mapper.instances[identifier].host_id
 
 
 # Strategy management for Conciliation
diff --git a/supvisors/supervisordata.py b/supvisors/supervisordata.py
index 9d0e15e1..64758164 100644
--- a/supvisors/supervisordata.py
+++ b/supvisors/supervisordata.py
@@ -84,13 +84,12 @@ def __init__(self, supvisors, supervisord):
         self.supervisord = supervisord
         self.logger: Logger = supvisors.logger
         # check HTTP configuration
-        self.server_config = None
         for config in supervisord.options.server_configs:
             if config['family'] == socket.AF_INET:
                 self.server_config = config
                 break
         else:
-            # server MUST be http, not unix
+            # there MUST be an inet HTTP server
             raise ValueError(f'Supervisor MUST be configured using inet_http_server: {supervisord.options.configfile}')
         # shortcuts (not available yet)
         self._system_rpc_interface = None
@@ -100,6 +99,16 @@ def __init__(self, supvisors, supervisord):
         self.disabilities = {}
         self.read_disabilities()
 
+    @property
+    def http_server(self):
+        """ Get the internal Supervisor HTTP server structure.
+
+        :return: the HTTP server structure
+        """
+        for config, hs in self.supervisord.options.httpservers:
+            if config['family'] == socket.AF_INET:
+                return hs
+
     @property
     def system_rpc_interface(self):
         """ Get the internal System Supervisor RPC handler.
@@ -109,7 +118,7 @@ def system_rpc_interface(self):
         """
         if not self._system_rpc_interface:
             # the first handler is the XML-RPC interface for rapid access
-            handler = self.httpserver.handlers[0]
+            handler = self.http_server.handlers[0]
             # if authentication used, handler is wrapped
             if self.username:
                 handler = handler.handler
@@ -125,7 +134,7 @@ def supervisor_rpc_interface(self):
         """
         if not self._supervisor_rpc_interface:
             # the first handler is the XML-RPC interface for rapid access
-            handler = self.httpserver.handlers[0]
+            handler = self.http_server.handlers[0]
             # if authentication used, handler is wrapped
             if self.username:
                 handler = handler.handler
@@ -141,7 +150,7 @@ def supvisors_rpc_interface(self):
         """
         if not self._supvisors_rpc_interface:
             # the first handler is the XML-RPC interface for rapid access
-            handler = self.httpserver.handlers[0]
+            handler = self.http_server.handlers[0]
             # if authentication is used, handler is wrapped
             if self.username:
                 handler = handler.handler
@@ -157,38 +166,39 @@ def identifier(self) -> str:
         return self.supervisord.options.identifier
 
     @property
-    def httpserver(self):
-        """ Get the internal Supervisor HTTP server structure.
-
-        :return: the HTTP server structure
-        """
-        for config, hs in self.supervisord.options.httpservers:
-            if config['family'] == socket.AF_INET:
-                return hs
-
-    @property
-    def serverurl(self) -> str:
-        return self.supervisord.options.serverurl
+    def server_host(self):
+        """ Return the host defined in the inet http server section. """
+        return self.server_config['host'] or 'localhost'
 
     @property
-    def serverport(self):
+    def server_port(self):
+        """ Return the port defined in the inet http server section. """
         return self.server_config['port']
 
     @property
     def username(self) -> str:
+        """ Return the user authentication defined in the inet http server section. """
         return self.server_config['username']
 
     @property
     def password(self) -> str:
+        """ Return the password authentication defined in the inet http server section. """
         return self.server_config['password']
 
+    @property
+    def server_url(self) -> str:
+        """ Return the server URL defined in the inet http server section. """
+        # do NOT use Supervisor serverurl as priority is given to the unix server if defined
+        return f'http://{self.server_host}:{self.server_port}'
+
     @property
     def supervisor_state(self):
+        """ Return the supervisord internal state. """
         return self.supervisord.options.mood
 
     def get_env(self) -> Dict[str, str]:
         """ Return a simple environment that can be used for the configuration of the XML-RPC client. """
-        return {'SUPERVISOR_SERVER_URL': self.serverurl,
+        return {'SUPERVISOR_SERVER_URL': self.server_url,
                 'SUPERVISOR_USERNAME': self.username,
                 'SUPERVISOR_PASSWORD': self.password}
 
@@ -241,8 +251,8 @@ def replace_tail_handlers(self) -> None:
             self.logger.debug('SupervisorData.replace_tail_handlers: Server running without any HTTP'
                               ' authentication checking')
         # replace Supervisor handlers considering the order in supervisor.http.make_http_servers
-        self.httpserver.handlers[1] = tail_handler
-        self.httpserver.handlers[2] = main_tail_handler
+        self.http_server.handlers[1] = tail_handler
+        self.http_server.handlers[2] = main_tail_handler
 
     def replace_default_handler(self) -> None:
         """ This method replaces Supervisor web UI with Supvisors web UI. """
@@ -260,7 +270,7 @@ def replace_default_handler(self) -> None:
             self.logger.debug('SupervisorData.replace_default_handler: Server running without any HTTP'
                               ' authentication checking')
         # replace Supervisor default handler at the end of the list
-        self.httpserver.handlers[-1] = def_handler
+        self.http_server.handlers[-1] = def_handler
 
     def close_httpservers(self) -> None:
         """ Call the close_httpservers of Supervisor.
diff --git a/supvisors/supvisorsctl.py b/supvisors/supvisorsctl.py
index 1acb905b..b1904339 100644
--- a/supvisors/supvisorsctl.py
+++ b/supvisors/supvisorsctl.py
@@ -172,11 +172,12 @@ def do_instance_status(self, arg):
                 # create template. identifier has variable length
                 max_identifiers = ControllerPlugin.max_template(info_list, 'identifier', 'Supervisor')
                 max_node_names = ControllerPlugin.max_template(info_list, 'node_name', 'Node')
-                template = (f'%(identifier)-{max_identifiers}s%(node_name)-{max_node_names}s%(port)-7s%(state)-11s'
-                            '%(load)-6s%(ltime)-10s%(counter)-9s%(fsm_state)-16s%(starting)-10s%(stopping)-10s')
+                template = (f'%(identifier)-{max_identifiers}s%(node_name)-{max_node_names}s%(port)-7s'
+                            '%(state)-11s%(load)-6s%(ltime)-10s%(counter)-9s%(failure)-9s'
+                            '%(fsm_state)-16s%(starting)-10s%(stopping)-10s')
                 # print title
                 payload = {'identifier': 'Supervisor', 'node_name': 'Node', 'port': 'Port', 'state': 'State',
-                           'load': 'Load', 'ltime': 'Time', 'counter': 'Counter',
+                           'load': 'Load', 'ltime': 'Time', 'counter': 'Counter', 'failure': 'Failure',
                            'fsm_state': 'FSM', 'starting': 'Starting', 'stopping': 'Stopping'}
                 self.ctl.output(template % payload)
                 # check request args
@@ -191,6 +192,7 @@ def do_instance_status(self, arg):
                                    'load': f"{info['loading']}%",
                                    'ltime': simple_localtime(info['local_time']),
                                    'counter': info['sequence_counter'],
+                                   'failure': info['process_failure'],
                                    'fsm_state': info['fsm_statename'],
                                    'starting': info['starting_jobs'],
                                    'stopping': info['stopping_jobs']}
diff --git a/supvisors/supvisorsmapper.py b/supvisors/supvisorsmapper.py
index c0cd6bce..85804e85 100644
--- a/supvisors/supvisorsmapper.py
+++ b/supvisors/supvisorsmapper.py
@@ -19,31 +19,25 @@
 
 import re
 from collections import OrderedDict
-from socket import gethostname, gethostbyaddr, herror, gaierror
-from typing import Any, Dict
+from socket import getfqdn, gethostbyaddr, herror, gaierror
+from typing import Any, Dict, Optional, Tuple
 
 from supervisor.loggers import Logger
 
 from .ttypes import NameList
 
 
-def get_node_names(ip_address: str, logger: Logger) -> NameList:
-    """ Get hostname, aliases and all IP addresses for the ip_address.
+def get_addresses(host_id: str, logger: Logger) -> Optional[Tuple[str, NameList, NameList]]:
+    """ Get hostname, aliases and all IP addresses for the host_id.
 
-    The first element of the returned list will be used as a unique node identifier in the event where multiple
-    Supervisor instances are used on the same node, and identified differently.
-    Preference is made to the IP address because it is not excluded that different nodes have the same host name.
-
-    :param ip_address: the host_name used in the Supvisors option
+    :param host_id: the host_name used in the Supvisors option
     :param logger: the Supvisors logger
-    :return: the list of possible node names
+    :return: the list of possible node name or IP addresses
     """
     try:
-        hostname, aliases, ip_addresses = gethostbyaddr(ip_address)
-        return ip_addresses + [hostname] + aliases
+        return gethostbyaddr(host_id)  # hostname, aliases, ip_addresses
     except (herror, gaierror):
-        logger.error(f'get_node_names: unknown host {ip_address}')
-        return []
+        logger.error(f'get_addresses: unknown address {host_id}')
 
 
 class SupvisorsInstanceId:
@@ -53,11 +47,12 @@ class SupvisorsInstanceId:
 
     Attributes are:
         - identifier: the Supervisor identifier or 'host_name[:http_port]' if not provided ;
-        - host_name: the host where the Supervisor is running ;
-        - ip_address: the known ip_address of the host ;
+        - host_id: the host where the Supervisor is running ;
         - http_port: the Supervisor port number ;
         - internal_port: the port number used to publish local events to remote Supvisors instances ;
-        - event_port: the port number used to publish all Supvisors events.
+        - event_port: the port number used to publish all Supvisors events ;
+        - host_name: the host name corresponding to the host id, as known by the local network configuration ;
+        - ip_address: the main ip_address of the host, as known by the local network configuration.
     """
 
     PATTERN = re.compile(r'^(<(?P<identifier>[\w\-]+)>)?(?P<host>[\w\-.]+)(:(?P<http_port>\d{4,5})?'
@@ -70,20 +65,25 @@ def __init__(self, item: str, supvisors: Any):
         """
         self.supvisors = supvisors
         self.logger: Logger = supvisors.logger
-        # attributes
+        # attributes deduced from input
         self.identifier = None
-        self.host_name = None
-        self.ip_address = None
+        self.host_id = None
         self.http_port = None
         self.internal_port = None
         self.event_port = None
         # parse item to get the values
         self.parse_from_string(item)
         self.check_values()
-        # choose the IP address among the possible node identifiers
-        node_names = get_node_names(self.host_name, self.logger)
-        if node_names:
-            self.ip_address = node_names[0]
+        # choose the node name among the possible node identifiers
+        self.host_name = None
+        self.ip_address = None
+        if self.host_id:
+            addresses = get_addresses(self.host_id, self.logger)
+            if addresses:
+                self.host_name, _, ip_addresses = addresses
+                self.ip_address = ip_addresses[0]
+                self.logger.debug(f'SupvisorsInstanceId: host_id={self.host_id} host_name={self.host_name}'
+                                  f' ip_address={self.ip_address}')
 
     def check_values(self) -> None:
         """ Complete information where not provided.
@@ -91,14 +91,14 @@ def check_values(self) -> None:
         :return: None
         """
         # define identifier
-        if not self.identifier and self.host_name:
-            self.identifier = self.host_name
+        if not self.identifier and self.host_id:
+            self.identifier = self.host_id
             # use HTTP port in identifier only if it has explicitly been defined
             if self.http_port:
                 self.identifier += f':{self.http_port}'
         # if http_port is not provided, use the local http_port value
         if not self.http_port:
-            self.http_port = self.supvisors.supervisor_data.serverport
+            self.http_port = self.supvisors.supervisor_data.server_port
         # if internal_port is not provided, use the option value if set or http_port+1
         if not self.internal_port:
             # assign to internal_port option value if set
@@ -118,7 +118,7 @@ def check_values(self) -> None:
                 # in this case, assign to http_port + 1
                 self.event_port = self.http_port + 1
         self.logger.debug(f'SupvisorsInstanceId.check_values: identifier={self.identifier}'
-                          f' host_name={self.host_name} http_port={self.http_port}'
+                          f' host_name={self.host_id} http_port={self.http_port}'
                           f' internal_port={self.internal_port}')
 
     def parse_from_string(self, item: str):
@@ -130,7 +130,7 @@ def parse_from_string(self, item: str):
         pattern_match = SupvisorsInstanceId.PATTERN.match(item)
         if pattern_match:
             self.identifier = pattern_match.group('identifier')
-            self.host_name = pattern_match.group('host')
+            self.host_id = pattern_match.group('host')
             # check http port
             port = pattern_match.group('http_port')
             self.http_port = int(port) if port else 0
@@ -138,7 +138,7 @@ def parse_from_string(self, item: str):
             port = pattern_match.group('internal_port')
             self.internal_port = int(port) if port else 0
             self.logger.debug(f'SupvisorsInstanceId.parse_from_string: identifier={self.identifier}'
-                              f' host_name={self.host_name} http_port={self.http_port}'
+                              f' host_id={self.host_id} http_port={self.http_port}'
                               f' internal_port={self.internal_port}')
 
     def __repr__(self) -> str:
@@ -160,8 +160,6 @@ class SupvisorsMapper(object):
         - _nodes: the list of Supvisors instances grouped by node names ;
         - _core_identifiers: the list of Supvisors core identifiers declared in the supvisors section of the Supervisor
           configuration file ;
-        - local_node_references: the list of known aliases of the current node, i.e. the host name
-          and the IPv4 addresses ;
         - local_identifier: the local Supvisors identifier.
     """
 
@@ -180,8 +178,6 @@ def __init__(self, supvisors: Any):
         self._instances: SupvisorsMapper.InstancesMap = OrderedDict()
         self._nodes: Dict[str, NameList] = {}
         self._core_identifiers: NameList = []
-        self.local_node_references = get_node_names(gethostname(), self.logger)
-        self.logger.debug(f'SupvisorsMapper: local_node_references={self.local_node_references}')
         self.local_identifier = None
 
     @property
@@ -249,20 +245,31 @@ def find_local_identifier(self):
 
         :return: the identifier of the local Supvisors
         """
+        # get the must-match parameters
+        local_host_name = getfqdn()
+        local_http_port = self.supvisors.supervisor_data.server_port
         # search for local Supervisor identifier first
         if self.supvisors.supervisor_data.identifier in self._instances:
             self.local_identifier = self.supvisors.supervisor_data.identifier
+            # check consistency
+            sup_id = self._instances[self.local_identifier]
+            if sup_id.host_name != local_host_name or sup_id.http_port != local_http_port:
+                self.logger.warn(f'SupvisorsMapper.find_local_identifier: {sup_id.host_name} != {local_host_name}'
+                                 f' or {sup_id.http_port} != {local_http_port}')
+                message = f'candidate {self.local_identifier} does not match the local Supvisors'
+                self.logger.error(f'SupvisorsMapper.find_local_identifier: {message}')
+                raise ValueError(message)
         else:
-            # if not found, try to find a Supvisors instance corresponding to local host name or known IP addresses
+            # if not found, try to find a Supvisors instance corresponding to the local host name
             # WARN: in this case, there MUST be exactly one unique matching Supvisors instance
-            matching_identifiers = [supervisor_id.identifier
-                                    for supervisor_id in self._instances.values()
-                                    if supervisor_id.host_name in self.local_node_references]
+            matching_identifiers = [sup_id.identifier
+                                    for sup_id in self._instances.values()
+                                    if sup_id.host_name == local_host_name and sup_id.http_port == local_http_port]
             if len(matching_identifiers) == 1:
                 self.local_identifier = matching_identifiers[0]
             else:
                 if len(matching_identifiers) > 1:
-                    message = f'multiple candidates for the local Supvisors: {matching_identifiers}'
+                    message = f'multiple candidates for the local Supvisors identifiers={matching_identifiers}'
                 else:
                     message = 'could not find the local Supvisors in supvisors_list'
                 self.logger.error(f'SupvisorsMapper.find_local_identifier: {message}')
diff --git a/supvisors/supvisorssocket.py b/supvisors/supvisorssocket.py
index 7f7cd7ef..fe64834f 100644
--- a/supvisors/supvisorssocket.py
+++ b/supvisors/supvisorssocket.py
@@ -371,13 +371,21 @@ def send_process_disability_event(self, payload: Payload) -> None:
         """
         self.publish(InternalEventHeaders.PROCESS_DISABILITY, payload)
 
-    def send_statistics(self, payload: Payload) -> None:
-        """ Publish the statistics.
+    def send_host_statistics(self, payload: Payload) -> None:
+        """ Publish the host statistics.
 
         :param payload: the statistics to publish
         :return: None
         """
-        self.publish(InternalEventHeaders.STATISTICS, payload)
+        self.publish(InternalEventHeaders.HOST_STATISTICS, payload)
+
+    def send_process_statistics(self, payload: Payload) -> None:
+        """ Publish the process statistics.
+
+        :param payload: the statistics to publish
+        :return: None
+        """
+        self.publish(InternalEventHeaders.PROCESS_STATISTICS, payload)
 
     def send_state_event(self, payload: Payload) -> None:
         """ Publish the Master state event.
@@ -501,7 +509,7 @@ class ClientConnectionThread(Thread):
     def __init__(self, instance: SupvisorsInstanceId, internal_subscriber):
         super().__init__(daemon=True)
         self.identifier = instance.identifier
-        self.address = instance.host_name, instance.internal_port
+        self.address = instance.host_id, instance.internal_port
         self.internal_subscriber = internal_subscriber
 
     def run(self):
diff --git a/supvisors/supvisorswebsocket.py b/supvisors/supvisorswebsocket.py
new file mode 100644
index 00000000..9aede450
--- /dev/null
+++ b/supvisors/supvisorswebsocket.py
@@ -0,0 +1,211 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# ======================================================================
+# Copyright 2023 Julien LE CLEACH
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ======================================================================
+
+import asyncio
+import json
+from typing import Dict, Set
+
+import websockets
+from supervisor.loggers import Logger
+
+from .eventinterface import EventPublisherInterface, EventSubscriber, AsyncEventThread
+from .supvisorsmapper import SupvisorsInstanceId
+from .ttypes import Payload, EventHeaders
+
+# additional annotation types
+WebSocketSubscriptions = Dict[websockets.WebSocketServerProtocol, Set[EventHeaders]]
+
+# the websocket subscriptions
+websocket_clients: WebSocketSubscriptions = {}
+
+
+# Server part
+async def ws_handler(websocket: websockets.WebSocketServerProtocol):
+    """ Manage websocket client subscriptions. """
+    websocket_clients[websocket] = set()
+    # manage subscriptions
+    subscription_headers = [elt for elt in websocket.path.split('/') if elt]
+    all_subscriptions = 'all' in subscription_headers
+    for header in EventHeaders:
+        if all_subscriptions or header.value in subscription_headers:
+            websocket_clients[websocket].add(header)
+    # wait until socket is closed (detected by the internal heartbeat)
+    await websocket.wait_closed()
+    # remove the websocket from the active connections
+    del websocket_clients[websocket]
+
+
+async def ws_server(stop_evt: asyncio.Event, node_name: str, event_port: int):
+    """ Run the websocket service until a stop event is received. """
+    async with websockets.serve(ws_handler, node_name, event_port, reuse_address=True) as server:
+        await stop_evt.wait()
+    await server.wait_closed()
+
+
+class WsEventPublisher(EventPublisherInterface):
+    """ Class for websockets publication of Supvisors events. """
+
+    def __init__(self, instance: SupvisorsInstanceId, logger: Logger):
+        """ Initialization of the attributes.
+
+        :param instance: the local Supvisors instance identification.
+        :param logger: the Supvisors logger.
+        """
+        self.logger: Logger = logger
+        logger.info(f'WsEventPublisher: initiating Websocket event publisher on {instance.event_port}')
+        # use binding on all interfaces
+        self.thread: AsyncEventThread = AsyncEventThread(ws_server, '', instance.event_port)
+        self.thread.start()
+
+    def close(self) -> None:
+        """ Close the Websocket.
+
+        :return: None
+        """
+        self.thread.stop()
+        self.thread.join()
+
+    def send_supvisors_status(self, status: Payload) -> None:
+        """ Send a JSON-serialized supvisors status through the socket.
+
+        :param status: the status to publish.
+        :return: None.
+        """
+        self.logger.trace(f'WsEventPublisher.send_supvisors_status: {status}')
+        clients = [ws for ws, subscriptions in websocket_clients.items()
+                   if EventHeaders.SUPVISORS in subscriptions]
+        websockets.broadcast(clients, json.dumps((EventHeaders.SUPVISORS.value, status)))
+
+    def send_instance_status(self, status: Payload) -> None:
+        """ Send a JSON-serialized Supvisors instance status through the socket.
+
+        :param status: the status to publish.
+        :return: None.
+        """
+        self.logger.trace(f'WsEventPublisher.send_instance_status: {status}')
+        clients = [ws for ws, subscriptions in websocket_clients.items()
+                   if EventHeaders.INSTANCE in subscriptions]
+        websockets.broadcast(clients, json.dumps((EventHeaders.INSTANCE.value, status)))
+
+    def send_application_status(self, status: Payload) -> None:
+        """ Send a JSON-serialized application status through the socket.
+
+        :param status: the status to publish.
+        :return: None.
+        """
+        self.logger.trace(f'WsEventPublisher.send_application_status: {status}')
+        clients = [ws for ws, subscriptions in websocket_clients.items()
+                   if EventHeaders.APPLICATION in subscriptions]
+        websockets.broadcast(clients, json.dumps((EventHeaders.APPLICATION.value, status)))
+
+    def send_process_event(self, identifier: str, event: Payload) -> None:
+        """ Send a JSON-serialized process event through the socket.
+
+        :param identifier: the identifier used to identify the origin of the event.
+        :param event: the event to publish.
+        :return: None.
+        """
+        # build the event before it is sent
+        evt = event.copy()
+        evt['identifier'] = identifier
+        self.logger.trace(f'WsEventPublisher.send_process_event: {evt}')
+        clients = [ws for ws, subscriptions in websocket_clients.items()
+                   if EventHeaders.PROCESS_EVENT in subscriptions]
+        websockets.broadcast(clients, json.dumps((EventHeaders.PROCESS_EVENT.value, evt)))
+
+    def send_process_status(self, status: Payload) -> None:
+        """ This method sends a serialized form of the process status through the socket.
+
+        :param status: the status to publish.
+        :return: None.
+        """
+        self.logger.trace(f'WsEventPublisher.send_process_status: {status}')
+        clients = [ws for ws, subscriptions in websocket_clients.items()
+                   if EventHeaders.PROCESS_STATUS in subscriptions]
+        websockets.broadcast(clients, json.dumps((EventHeaders.PROCESS_STATUS.value, status)))
+
+    def send_host_statistics(self, statistics: Payload) -> None:
+        """ This method sends host statistics through the socket.
+
+        :param statistics: the statistics to publish.
+        :return: None.
+        """
+        self.logger.trace(f'WsEventPublisher.send_host_statistics: {statistics}')
+        clients = [ws for ws, subscriptions in websocket_clients.items()
+                   if EventHeaders.HOST_STATISTICS in subscriptions]
+        websockets.broadcast(clients, json.dumps((EventHeaders.HOST_STATISTICS.value, statistics)))
+
+    def send_process_statistics(self, statistics: Payload) -> None:
+        """ This method sends process statistics through the socket.
+
+        :param statistics: the statistics to publish.
+        :return: None;
+        """
+        self.logger.trace(f'WsEventPublisher.send_process_statistics: {statistics}')
+        clients = [ws for ws, subscriptions in websocket_clients.items()
+                   if EventHeaders.PROCESS_STATISTICS in subscriptions]
+        websockets.broadcast(clients, json.dumps((EventHeaders.PROCESS_STATISTICS.value, statistics)))
+
+
+# Subscriber part
+class WsEventSubscriber(EventSubscriber):
+    """ The WsEventSubscriber wraps the websocket that connects to Supvisors.
+
+    The WsEventSubscriber requires:
+        - an implementation of the event subscriber interface,
+        - the event port number used by Supvisors to publish its events,
+        - a logger.
+    """
+
+    # timeout to avoid blocking receptions
+    RecvTimeout = 0.5
+
+    async def mainloop(self, stop_evt: asyncio.Event, node_name: str, event_port: int) -> None:
+        """ Infinite loop as a websocket client.
+
+        :return: None.
+        """
+        headers = 'all' if self.all_subscriptions() else '/'.join(self.headers)
+        uri = f'ws://{node_name}:{event_port}/{headers}'
+        while not stop_evt.is_set():
+            # open the websocket connection
+            self.logger.debug(f'WsEventSubscriber: connecting {uri}')
+            async with websockets.connect(uri) as ws:
+                self.logger.info(f'WsEventSubscriber.mainloop: Websocket connected on {uri}')
+                try:
+                    while not stop_evt.is_set():
+                        try:
+                            # recv in wait_for so that stop_event can be checked periodically
+                            message = await asyncio.wait_for(ws.recv(), timeout=WsEventSubscriber.RecvTimeout)
+                        except asyncio.TimeoutError:
+                            self.logger.trace(f'WsEventSubscriber.mainloop: receive timeout on {uri}')
+                            continue
+                        # a message has been received
+                        header, body = json.loads(message)
+                        try:
+                            self.on_receive(header, body)
+                        except ValueError:
+                            self.logger.error(f'WsEventSubscriber.mainloop: unexpected header={header}')
+                    self.logger.debug(f'WsEventSubscriber.mainloop: exiting Websocket connection on {uri}')
+                except websockets.ConnectionClosed:
+                    self.logger.warn(f'WsEventSubscriber.mainloop: Websocket server connection closed from {uri}')
+                # close the websocket connection
+                self.logger.debug(f'WsEventSubscriber.mainloop: close Websocket connection on {uri}')
+                await ws.close()
+        self.logger.debug('WsEventSubscriber.mainloop: exiting Websocket connection loop')
diff --git a/supvisors/supvisorszmq.py b/supvisors/supvisorszmq.py
index 2c299bb0..594c5c43 100644
--- a/supvisors/supvisorszmq.py
+++ b/supvisors/supvisorszmq.py
@@ -17,12 +17,13 @@
 # limitations under the License.
 # ======================================================================
 
-import zmq
+import asyncio
 
+import zmq
+import zmq.asyncio
 from supervisor.loggers import Logger
-from typing import Any, Tuple
 
-from .publisherinterface import EventPublisherInterface
+from .eventinterface import EventPublisherInterface, EventSubscriber, EventSubscriberInterface
 from .supvisorsmapper import SupvisorsInstanceId
 from .ttypes import Payload, EventHeaders
 
@@ -33,7 +34,7 @@
 ZmqContext = zmq.Context.instance()
 
 
-class EventPublisher(EventPublisherInterface):
+class ZmqEventPublisher(EventPublisherInterface):
     """ Class for PyZmq publication of Supvisors events. """
 
     def __init__(self, instance: SupvisorsInstanceId, logger: Logger):
@@ -43,11 +44,10 @@ def __init__(self, instance: SupvisorsInstanceId, logger: Logger):
         :param logger: the Supvisors logger
         """
         self.logger: Logger = logger
-        logger.info('EventPublisher: initiating PyZmq event publisher')
+        logger.info(f'ZmqEventPublisher: initiating PyZmq event publisher on {instance.event_port}')
         self.socket: zmq.Socket = ZmqContext.socket(zmq.PUB)
-        # WARN: this is a local binding, only visible to processes located on the same host
-        url = f'tcp://127.0.0.1:{instance.event_port}'
-        self.logger.debug(f'EventPublisher: binding {url}')
+        url = f'tcp://0.0.0.0:{instance.event_port}'
+        self.logger.debug(f'ZmqEventPublisher: binding {url}')
         self.socket.bind(url)
 
     def close(self) -> None:
@@ -63,7 +63,7 @@ def send_supvisors_status(self, status: Payload) -> None:
         :param status: the status to publish
         :return: None
         """
-        self.logger.trace(f'EventPublisher.send_supvisors_status: {status}')
+        self.logger.trace(f'ZmqEventPublisher.send_supvisors_status: {status}')
         self.socket.send_string(EventHeaders.SUPVISORS.value, zmq.SNDMORE)
         self.socket.send_json(status)
 
@@ -73,7 +73,7 @@ def send_instance_status(self, status: Payload) -> None:
         :param status: the status to publish
         :return: None
         """
-        self.logger.trace(f'EventPublisher.send_instance_status: {status}')
+        self.logger.trace(f'ZmqEventPublisher.send_instance_status: {status}')
         self.socket.send_string(EventHeaders.INSTANCE.value, zmq.SNDMORE)
         self.socket.send_json(status)
 
@@ -83,7 +83,7 @@ def send_application_status(self, status: Payload) -> None:
         :param status: the status to publish
         :return: None
         """
-        self.logger.trace(f'EventPublisher.send_application_status: {status}')
+        self.logger.trace(f'ZmqEventPublisher.send_application_status: {status}')
         self.socket.send_string(EventHeaders.APPLICATION.value, zmq.SNDMORE)
         self.socket.send_json(status)
 
@@ -97,7 +97,7 @@ def send_process_event(self, identifier: str, event: Payload) -> None:
         # build the event before it is sent
         evt = event.copy()
         evt['identifier'] = identifier
-        self.logger.trace(f'EventPublisher.send_process_event: {evt}')
+        self.logger.trace(f'ZmqEventPublisher.send_process_event: {evt}')
         self.socket.send_string(EventHeaders.PROCESS_EVENT.value, zmq.SNDMORE)
         self.socket.send_json(evt)
 
@@ -107,156 +107,101 @@ def send_process_status(self, status: Payload) -> None:
         :param status: the status to publish
         :return: None
         """
-        self.logger.trace(f'EventPublisher.send_process_status: {status}')
+        self.logger.trace(f'ZmqEventPublisher.send_process_status: {status}')
         self.socket.send_string(EventHeaders.PROCESS_STATUS.value, zmq.SNDMORE)
         self.socket.send_json(status)
 
+    def send_host_statistics(self, statistics: Payload) -> None:
+        """ This method sends host statistics through the socket.
 
-class EventSubscriber(object):
-    """ The EventSubscriber wraps the PyZmq socket that connects to **Supvisors**.
-
-    The TCP socket is configured with a PyZmq ``SUBSCRIBE`` pattern.
-    It is connected to the **Supvisors** instance running on the localhost and bound on the event port.
-
-    The EventSubscriber requires:
-        - the event port number used by **Supvisors** to publish its events,
-        - a logger reference to log traces.
-
-    Attributes:
-        - logger: the reference to the logger,
-        - socket: the PyZmq socket connected to **Supvisors**.
-    """
-
-    def __init__(self, zmq_context: zmq.Context, port: int, logger: Logger) -> None:
-        """ Initialization of the attributes.
-
-        :param zmq_context: the PyZmq context instance
-        :param port: the port number of the TCP socket
-        :param logger: the Supvisors logger
-        """
-        self.logger = logger
-        # create ZeroMQ socket
-        self.socket = zmq_context.socket(zmq.SUB)
-        # WARN: this is a local binding, only visible to processes located on the same host
-        url = f'tcp://127.0.0.1:{port}'
-        self.logger.debug(f'EventSubscriber: connecting {url}')
-        self.socket.connect(url)
-
-    def close(self) -> None:
-        """ Close the PyZmq socket.
-
-        :return: None
-        """
-        self.socket.close(ZMQ_LINGER)
-
-    # subscription part
-    def subscribe_all(self) -> None:
-        """ Subscribe to all events.
-
-        :return: None
-        """
-        self.socket.setsockopt(zmq.SUBSCRIBE, b'')
-
-    def subscribe_supvisors_status(self) -> None:
-        """ Subscribe to Supvisors Status messages.
-
-        :return: None
-        """
-        self.subscribe(EventHeaders.SUPVISORS.value)
-
-    def subscribe_instance_status(self) -> None:
-        """ Subscribe to Supvisors Instance Status messages.
-
-        :return: None
-        """
-        self.subscribe(EventHeaders.INSTANCE.value)
-
-    def subscribe_application_status(self) -> None:
-        """ Subscribe to Application Status messages.
-
+        :param statistics: the statistics to publish
         :return: None
         """
-        self.subscribe(EventHeaders.APPLICATION.value)
+        self.logger.trace(f'ZmqEventPublisher.send_host_statistics: {statistics}')
+        self.socket.send_string(EventHeaders.HOST_STATISTICS.value, zmq.SNDMORE)
+        self.socket.send_json(statistics)
 
-    def subscribe_process_event(self) -> None:
-        """ Subscribe to Process Event messages.
+    def send_process_statistics(self, statistics: Payload) -> None:
+        """ This method sends process statistics through the socket.
 
+        :param statistics: the statistics to publish
         :return: None
         """
-        self.subscribe(EventHeaders.PROCESS_EVENT.value)
+        self.logger.trace(f'ZmqEventPublisher.send_process_statistics: {statistics}')
+        self.socket.send_string(EventHeaders.PROCESS_STATISTICS.value, zmq.SNDMORE)
+        self.socket.send_json(statistics)
 
-    def subscribe_process_status(self) -> None:
-        """ Subscribe to Process Status messages.
 
-        :return: None
-        """
-        self.subscribe(EventHeaders.PROCESS_STATUS.value)
-
-    def subscribe(self, code: str) -> None:
-        """ Subscribe to the event named code.
-
-        :param code: the message code for subscription
-        :return: None
-        """
-        self.socket.setsockopt(zmq.SUBSCRIBE, code.encode('utf-8'))
-
-    # unsubscription part
-    def unsubscribe_all(self) -> None:
-        """ Unsubscribe from all events.
-
-        :return: None
-        """
-        self.socket.setsockopt(zmq.UNSUBSCRIBE, b'')
-
-    def unsubscribe_supvisors_status(self) -> None:
-        """ Unsubscribe from Supvisors Status messages
-
-        :return: None
-        """
-        self.unsubscribe(EventHeaders.SUPVISORS.value)
-
-    def unsubscribe_instance_status(self) -> None:
-        """ Unsubscribe from Supvisors Instance Status messages
-
-        :return: None
-        """
-        self.unsubscribe(EventHeaders.INSTANCE.value)
+class ZmqEventSubscriber(EventSubscriber):
+    """ The ZmqEventSubscriber wraps the PyZmq socket that connects to Supvisors.
 
-    def unsubscribe_application_status(self) -> None:
-        """ Unsubscribe from Application Status messages
+    The TCP socket is configured with a PyZmq SUBSCRIBE pattern.
+    It is connected to the Supvisors instance running on the localhost and bound on the event port.
 
-        :return: None
-        """
-        self.unsubscribe(EventHeaders.APPLICATION.value)
-
-    def unsubscribe_process_event(self) -> None:
-        """ Unsubscribe from Process Event messages
-
-        :return: None
-        """
-        self.unsubscribe(EventHeaders.PROCESS_EVENT.value)
-
-    def unsubscribe_process_status(self) -> None:
-        """ Unsubscribe from Process Status messages
+    The ZmqEventSubscriber requires:
+        - the event port number used by Supvisors to publish its events,
+        - a logger reference to log traces.
 
-        :return: None
-        """
-        self.unsubscribe(EventHeaders.PROCESS_STATUS.value)
+    Attributes:
+        - logger: the reference to the logger,
+        - socket: the PyZmq socket connected to Supvisors.
 
-    def unsubscribe(self, code: str) -> None:
-        """ Remove subscription to the event named code.
+    Constants:
+        - _Poll_timeout: duration used to time out the PyZMQ poller, defaulted to 500 milliseconds.
+    """
 
-        :param code: the message code for unsubscription
-        :return: None
-        """
-        self.socket.setsockopt(zmq.UNSUBSCRIBE, code.encode('utf-8'))
+    # timeout to avoid blocking reception
+    _Poll_timeout = 500
 
-    # reception part
-    def receive(self) -> Tuple[str, Any]:
-        """ Reception of two-parts message:
-            - header as a unicode string,
-            - data encoded in JSON.
+    def __init__(self, zmq_context: zmq.asyncio.Context, intf: EventSubscriberInterface,
+                 node_name: str, event_port: int, logger: Logger) -> None:
+        """ Initialization of the attributes.
 
-        :return: a tuple with header and body
+        :param zmq_context: the PyZmq context instance
+        :param intf: the event subscriber interface used to provide the messages received
+        :param node_name: the node name of the Supvisors instance that publishes events
+        :param event_port: the port used by the Supvisors instance to publish events from the node
+        :param logger: the Supvisors logger
         """
-        return self.socket.recv_string(), self.socket.recv_json()
+        super().__init__(intf, node_name, event_port, logger)
+        self.zmq_context = zmq_context
+
+    async def mainloop(self, stop_evt: asyncio.Event, node_name: str, event_port: int) -> None:
+        """ Main loop of the thread. """
+        # create the PyZMQ socket
+        socket: zmq.asyncio.Socket = self.zmq_context.socket(zmq.SUB)
+        # set subscriptions
+        if self.all_subscriptions():
+            socket.setsockopt(zmq.SUBSCRIBE, b'')
+        else:
+            for header in self.headers:
+                socket.setsockopt(zmq.SUBSCRIBE, header.encode('utf-8'))
+        # define the URI
+        uri = f'tcp://{node_name}:{event_port}'
+        while not stop_evt.is_set():
+            self.logger.debug(f'ZmqEventSubscriber: connecting {uri}')
+            socket.connect(uri)
+            # create poller and register event subscriber
+            poller = zmq.asyncio.Poller()
+            poller.register(socket, zmq.POLLIN)
+            # poll events every seconds
+            self.logger.info('ZmqEventSubscriber.run: entering main loop')
+            while not stop_evt.is_set():
+                socks = dict(await poller.poll(ZmqEventSubscriber._Poll_timeout))
+                # check if something happened on the socket
+                if socket in socks and socks[socket] == zmq.POLLIN:
+                    self.logger.debug('ZmqEventSubscriber.run: got message on subscriber')
+                    try:
+                        header = await socket.recv_string()
+                        body = await socket.recv_json()
+                    except zmq.ZMQError as exc:
+                        self.logger.error(f'ZmqEventSubscriber.run: socket error on subscriber: {exc}')
+                    except UnicodeDecodeError:
+                        self.logger.error(f'ZmqEventSubscriber.run: incompatible data type received from {uri}')
+                    else:
+                        try:
+                            self.on_receive(header, body)
+                        except ValueError:
+                            self.logger.error(f'ZmqEventSubscriber.run: unexpected event type {header}')
+            self.logger.warn('ZmqEventSubscriber.run: exiting main loop')
+            socket.close(ZMQ_LINGER)
diff --git a/supvisors/tools/apis/supvisors_namespace.py b/supvisors/tools/apis/supvisors_namespace.py
index e1751d4d..65b22953 100644
--- a/supvisors/tools/apis/supvisors_namespace.py
+++ b/supvisors/tools/apis/supvisors_namespace.py
@@ -18,7 +18,7 @@
 # ======================================================================
 
 from flask import g, jsonify
-from flask_restx import Namespace, Resource, inputs
+from flask_restx import Namespace, Resource, fields, inputs
 
 from supvisors.rpcinterface import RPCInterface
 from supvisors.ttypes import ConciliationStrategies, StartingStrategies
@@ -44,40 +44,49 @@
 start_process_parser.add_argument('wait', type=inputs.boolean, default=True,
                                   help='if ``True``, wait until completion of the request')
 
+# Models
+wait_model = api.model('Wait', {'wait': fields.Boolean})
+start_process_model = api.clone('Start process', wait_model, {'extra_args': fields.String})
+
 
 # Routes
 @api.route('/api_version', methods=('GET',))
 @api.doc(description=get_docstring_description(RPCInterface.get_api_version))
 class SupvisorsApiVersion(Resource):
-    def get(self):
+    @staticmethod
+    def get():
         return g.proxy.supvisors.get_api_version()
 
 
 @api.route('/supvisors_state', methods=('GET',))
 @api.doc(description=get_docstring_description(RPCInterface.get_supvisors_state))
 class SupvisorsState(Resource):
-    def get(self):
+    @staticmethod
+    def get():
         return g.proxy.supvisors.get_supvisors_state()
 
 
 @api.route('/master_identifier', methods=('GET',))
 @api.doc(description=get_docstring_description(RPCInterface.get_master_identifier))
 class SupvisorsMasterIdentifier(Resource):
-    def get(self):
+    @staticmethod
+    def get():
         return g.proxy.supvisors.get_master_identifier()
 
 
 @api.route('/strategies', methods=('GET',))
 @api.doc(description=get_docstring_description(RPCInterface.get_strategies))
 class SupvisorsStrategies(Resource):
-    def get(self):
+    @staticmethod
+    def get():
         return g.proxy.supvisors.get_strategies()
 
 
 @api.route('/all_instances_info', methods=('GET',))
 @api.doc(description=get_docstring_description(RPCInterface.get_all_instances_info))
 class SupvisorsAllInstancesInfo(Resource):
-    def get(self):
+    @staticmethod
+    def get():
         return jsonify(g.proxy.supvisors.get_all_instances_info())
 
 
@@ -92,7 +101,8 @@ def get(self, identifier):
 @api.route('/all_applications_info', methods=('GET',))
 @api.doc(description=get_docstring_description(RPCInterface.get_all_applications_info))
 class SupvisorsAllApplicationsInfo(Resource):
-    def get(self):
+    @staticmethod
+    def get():
         return jsonify(g.proxy.supvisors.get_all_applications_info())
 
 
@@ -115,7 +125,8 @@ def get(self, application_name):
 @api.route('/all_process_info', methods=('GET',))
 @api.doc(description=get_docstring_description(RPCInterface.get_all_process_info))
 class SupvisorsAllProcessInfo(Resource):
-    def get(self):
+    @staticmethod
+    def get():
         return jsonify(g.proxy.supvisors.get_all_process_info())
 
 
@@ -130,7 +141,8 @@ def get(self, namespec):
 @api.route('/all_local_process_info', methods=('GET',))
 @api.doc(description=get_docstring_description(RPCInterface.get_all_local_process_info))
 class SupvisorsAllLocalProcessInfo(Resource):
-    def get(self):
+    @staticmethod
+    def get():
         return jsonify(g.proxy.supvisors.get_all_local_process_info())
 
 
@@ -153,7 +165,8 @@ def get(self, namespec):
 @api.route('/conflicts', methods=('GET',))
 @api.doc(description=get_docstring_description(RPCInterface.get_conflicts))
 class SupvisorsConflicts(Resource):
-    def get(self):
+    @staticmethod
+    def get():
         return jsonify(g.proxy.supvisors.get_conflicts())
 
 
@@ -192,14 +205,14 @@ def post(self, strategy, application_name):
         return g.proxy.supvisors.restart_application(strategy, application_name, args.wait)
 
 
-@api.route(f'/start_args/<string:namespec>/<string:extra_args>', methods=('POST',))
+@api.route(f'/start_args/<string:namespec>', methods=('POST',))
 @api.doc(description=get_docstring_description(RPCInterface.start_args))
 class SupvisorsStartArgs(Resource):
     @api.doc(params=get_docstring_parameters(RPCInterface.start_args))
-    @api.expect(wait_parser)
-    def post(self, namespec, extra_args):
-        args = wait_parser.parse_args()
-        return g.proxy.supvisors.start_args(namespec, extra_args, args.wait)
+    @api.expect(start_process_parser)
+    def post(self, namespec):
+        args = start_process_parser.parse_args()
+        return g.proxy.supvisors.start_args(namespec, args.extra_args, args.wait)
 
 
 @api.route(f'/start_process/<any({StartingStrategiesParam}):strategy>/<string:namespec>', methods=('POST',))
@@ -299,14 +312,16 @@ def post(self):
 @api.route(f'/restart', methods=('POST',))
 @api.doc(description=get_docstring_description(RPCInterface.restart))
 class SupvisorsRestart(Resource):
-    def post(self):
+    @staticmethod
+    def post():
         return g.proxy.supvisors.restart()
 
 
 @api.route(f'/shutdown', methods=('POST',))
 @api.doc(description=get_docstring_description(RPCInterface.shutdown))
 class SupvisorsShutdown(Resource):
-    def post(self):
+    @staticmethod
+    def post():
         return g.proxy.supvisors.shutdown()
 
 
diff --git a/supvisors/ttypes.py b/supvisors/ttypes.py
index d8566e54..e6f5b9fe 100644
--- a/supvisors/ttypes.py
+++ b/supvisors/ttypes.py
@@ -41,7 +41,7 @@ class ApplicationStates(Enum):
 
 class EventLinks(Enum):
     """ Available link types used to publish all Supvisors events. """
-    NONE, ZMQ = range(2)
+    NONE, ZMQ, WS = range(3)
 
 
 class StartingStrategies(Enum):
@@ -75,16 +75,23 @@ class DistributionRules(Enum):
     ALL_INSTANCES, SINGLE_INSTANCE, SINGLE_NODE = range(3)
 
 
+class StatisticsTypes(Enum):
+    """ Items to be captured when statistics are enabled. """
+    OFF, HOST, PROCESS, ALL = range(4)
+
+
 # for internal publish / subscribe
 class InternalEventHeaders(Enum):
     """ Enumeration class for the headers in messages between Listener and MainLoop. """
-    HEARTBEAT, TICK, PROCESS, PROCESS_ADDED, PROCESS_REMOVED, PROCESS_DISABILITY, STATISTICS, STATE = range(8)
+    (HEARTBEAT, TICK, PROCESS, PROCESS_ADDED, PROCESS_REMOVED, PROCESS_DISABILITY,
+     HOST_STATISTICS, PROCESS_STATISTICS, STATE) = range(9)
 
 
 # for deferred XML-RPC requests
 class DeferredRequestHeaders(Enum):
     """ Enumeration class for the headers of deferred XML-RPC messages sent to MainLoop. """
-    (CHECK_INSTANCE, ISOLATE_INSTANCES, START_PROCESS, STOP_PROCESS,
+    (CHECK_INSTANCE, ISOLATE_INSTANCES,
+     START_PROCESS, STOP_PROCESS,
      RESTART, SHUTDOWN, RESTART_SEQUENCE, RESTART_ALL, SHUTDOWN_ALL) = range(9)
 
 
@@ -102,6 +109,8 @@ class EventHeaders(Enum):
     APPLICATION = 'application'
     PROCESS_EVENT = 'event'
     PROCESS_STATUS = 'process'
+    HOST_STATISTICS = 'hstats'
+    PROCESS_STATISTICS = 'pstats'
 
 
 # State lists commonly used
@@ -168,11 +177,10 @@ class ProcessDisabledEvent(ProcessEvent):
 LoadMap = Dict[str, int]
 
 # Annotation types for statistics
-NamedPid = Tuple[str, int]  # namespec, PID
-NamedPidList = List[NamedPid]
 Jiffies = Tuple[float, float]  # (work, idle)
 JiffiesList = List[Jiffies]  # one entry per processor + 1 for average (first element)
 CPUInstantStats = List[float]  # in percent. one entry per processor + 1 for average (first element)
+TimesHistoryStats = List[float]  # in seconds
 CPUHistoryStats = List[List[float]]  # in percent. one list per processor + 1 for average (first element)
 MemHistoryStats = List[float]  # in percent
 IOBytes = Tuple[int, int]  # recv_bytes, sent_bytes
@@ -180,8 +188,6 @@ class ProcessDisabledEvent(ProcessEvent):
 InterfaceInstantStats = Dict[str, IOBytes]  # {interface: (recv_bytes, sent_bytes)}
 InterfaceIntegratedStats = Dict[str, Tuple[float, float]]  # {interface: (recv_bytes, sent_bytes)}
 InterfaceHistoryStats = Dict[str, Tuple[BytesList, BytesList]]  # {interface: ([recv_bytes], [sent_bytes])}
-ProcessStats = Tuple[float, float]  # jiffies, memory
-ProcessStatsMap = Dict[str, Tuple[int, ProcessStats]]  # {namespec: (PID, ProcessStats)}
-ProcessHistoryStats = Tuple[CPUInstantStats, MemHistoryStats]
-ProcessHistoryStatsMap = Dict[NamedPid, ProcessHistoryStats]
-InstantStatistics = Tuple[float, JiffiesList, float, InterfaceInstantStats, ProcessStatsMap]
+ProcessStats = Tuple[float, float]  # work jiffies, memory
+ProcessCPUHistoryStats = List[float]  # in percent
+ProcessMemHistoryStats = MemHistoryStats  # in percent
diff --git a/supvisors/ui/application.html b/supvisors/ui/application.html
index 62380430..2b97711b 100644
--- a/supvisors/ui/application.html
+++ b/supvisors/ui/application.html
@@ -6,6 +6,7 @@
     <meta charset="utf-8" />
     <meta http-equiv="refresh" content="5" meld:id="meta_mid"/>
     <title>Supvisors status</title>
+    <link href="img/icon.png" rel="icon" type="image/png" />
     <link rel="stylesheet" href="css/ui_style.css" />
     <link rel="stylesheet" href="css/menu.css" />
     <link rel="stylesheet" href="css/button.css" />
@@ -17,7 +18,7 @@
 
         <div id="left_side">
             <header>
-                <h1><a href="index.html" meld:id="supvisors_mid">Supvisors</a></h1>
+                <h1><a href="index.html" meld:id="supvisors_mid"><img src="img/supvisors.png"/></a></h1>
                 <p><span meld:id="version_mid"></span></p>
                 <p>view from <span meld:id="identifier_mid"></span></p>
             </header>
@@ -42,7 +43,7 @@ <h2 meld:id="appli_h_mid">Applications</h2>
 
             <footer>
                 <p>Contact: <a href="mailto:julien.6387.dev@gmail.com">Julien Le Cl&amp;eacute;ach</a></p>
-                <p id="copyright">&amp;copy; Copyright 2016-2022 Julien LE CLEACH</p>
+                <p id="copyright">&amp;copy; Copyright 2016-2023 Julien LE CLEACH</p>
             </footer>
         </div>
 
@@ -163,41 +164,53 @@ <h3>
                 <div id="process_right_side" meld:id="pstats_div_mid" class="vertical_contents">
                     <h4>Detailed Statistics -- <span meld:id="process_h_mid">*</span> on <span meld:id="instance_fig_mid">--</span></h4>
 
-                    <div>
-                        <table>
-                            <tr>
-                                <th colspan="4">MEM</th>
-                                <th colspan="4">CPU</th>
-                            </tr>
-                            <tr>
-                                <th>%</th>
-                                <th>Mean %</th>
-                                <th>Slope %</th>
-                                <th>SD %</th>
-                                <th>%</th>
-                                <th>Mean %</th>
-                                <th>Slope %</th>
-                                <th>SD %</th>
-                            </tr>
-                            <tr class="brightened">
-                                <td meld:id="pmemval_td_mid">--</td>
-                                <td meld:id="pmemavg_td_mid">--</td>
-                                <td meld:id="pmemslope_td_mid">--</td>
-                                <td meld:id="pmemdev_td_mid">--</td>
-                                <td meld:id="pcpuval_td_mid">--</td>
-                                <td meld:id="pcpuavg_td_mid">--</td>
-                                <td meld:id="pcpuslope_td_mid">--</td>
-                                <td meld:id="pcpudev_td_mid">--</td>
-                            </tr>
-                        </table>
+                    <div class="stats_contents">
+                        <div>
+                            <table>
+                                <tr>
+                                    <th colspan="4">CPU</th>
+                                </tr>
+                                <tr>
+                                    <th>%</th>
+                                    <th>Mean %</th>
+                                    <th>Slope %</th>
+                                    <th>SD %</th>
+                                </tr>
+                                <tr class="brightened">
+                                    <td meld:id="pcpuval_td_mid">--</td>
+                                    <td meld:id="pcpuavg_td_mid">--</td>
+                                    <td meld:id="pcpuslope_td_mid">--</td>
+                                    <td meld:id="pcpudev_td_mid">--</td>
+                                </tr>
+                            </table>
+                        </div>
+                        <div>
+                            <table>
+                                <tr>
+                                    <th colspan="4">MEM</th>
+                                </tr>
+                                <tr>
+                                    <th>%</th>
+                                    <th>Mean %</th>
+                                    <th>Slope %</th>
+                                    <th>SD %</th>
+                                </tr>
+                                <tr class="brightened">
+                                    <td meld:id="pmemval_td_mid">--</td>
+                                    <td meld:id="pmemavg_td_mid">--</td>
+                                    <td meld:id="pmemslope_td_mid">--</td>
+                                    <td meld:id="pmemdev_td_mid">--</td>
+                                </tr>
+                            </table>
+                        </div>
                     </div>
 
                     <figure>
-                        <img src="process_mem.png" alt="Process Memory Graph"/>
+                        <img src="process_cpu.png" alt="Process CPU Graph"/>
                     </figure>
 
                     <figure>
-                        <img src="process_cpu.png" alt="Process CPU Graph"/>
+                        <img src="process_mem.png" alt="Process Memory Graph"/>
                     </figure>
                 </div>
             </div>
diff --git a/supvisors/ui/css/ui_style.css b/supvisors/ui/css/ui_style.css
index b427b5bc..19b3d586 100644
--- a/supvisors/ui/css/ui_style.css
+++ b/supvisors/ui/css/ui_style.css
@@ -178,6 +178,17 @@ nav {
     justify-content: space-around;
 }
 
+.stats_contents {
+    display: flex;
+    flex: 0;
+    flex-direction: row;
+    justify-content: space-between;
+}
+
+.stats_contents div>* {
+    margin-left: 5px;
+}
+
 #rsc_grid {
     display: grid;
     grid-template-columns: 1fr 300px 1fr;
@@ -266,14 +277,18 @@ header, nav, footer {
     padding: 5px;
 }
 
+header {
+    margin-bottom: 1em;
+}
+
 h1, h2, h3, h4 {
     font-family: 'kingthings_calligraphica_2Rg', Verdana, sans-serif;
 }
 
 h1 {
-    font-size: 1.6em;
+    text-align: left;
     margin-top: 0.2em;
-    margin-bottom: 0.4em;
+    margin-bottom: 0.2em;
 }
 
 h2, h3 {
@@ -325,7 +340,7 @@ a {
  * Blink Style
  *
  */
-#left_side header a.blink, .blink {
+.blink {
     color: var(--blink-color);
     animation: blinker 1.2s linear infinite;
 }
@@ -452,26 +467,27 @@ ul.vertical li {
  * Common Table style
  */
 table {
-    border-collapse: separate; #collapse;
+    border-collapse: collapse;
     border-spacing: 0;
     caption-side: top;
     background-color: var(--tr-color);
     margin: 0 auto;
 }
 
-caption {
-    padding-right: 4px;
-    font-size: .8em;
-    font-style: italic;
-}
-
-/* add border for all cells */
 th, td {
-    border: 1px solid var(--border-color);
     padding: 3px 5px;
     font-size: .7em;
 }
 
+tr {
+    border: none;
+}
+
+th, td {
+    border-top: solid 1px var(--border-color);
+    border-bottom: solid 1px var(--border-color);
+}
+
 th {
     background-color: var(--th-color);
     text-align: center;
@@ -483,6 +499,12 @@ th {
     z-index: 20;
 }
 
+caption {
+    padding-right: 4px;
+    font-size: .8em;
+    font-style: italic;
+}
+
 .brightened {
     background-image: linear-gradient(180deg, var(--vlight2-color), var(--light2-color), var(--vlight2-color));
 }
@@ -638,6 +660,14 @@ nav li.failure::after {
     top: -30px;
 }
 
+h1 a.failure::after {
+    content: url(../img/red_light_30.png);
+    float: right;
+    position: relative;
+    top: -32px;
+    animation: blinker 1.2s linear infinite;
+}
+
 h2.failure::after {
     content: url(../img/red_light_30.png);
     float: right;
diff --git a/supvisors/ui/host_instance.html b/supvisors/ui/host_instance.html
index 31397179..a0b10663 100644
--- a/supvisors/ui/host_instance.html
+++ b/supvisors/ui/host_instance.html
@@ -6,6 +6,7 @@
     <meta charset="utf-8" />
     <meta http-equiv="refresh" content="5" meld:id="meta_mid"/>
     <title>Supvisors status</title>
+    <link href="img/icon.png" rel="icon" type="image/png" />
     <link rel="stylesheet" href="css/ui_style.css" />
     <link rel="stylesheet" href="css/menu.css" />
     <link rel="stylesheet" href="css/button.css" />
@@ -17,7 +18,7 @@
 
         <div id="left_side">
             <header>
-                <h1><a href="index.html" meld:id="supvisors_mid">Supvisors</a></h1>
+                <h1><a href="index.html" meld:id="supvisors_mid"><img src="img/supvisors.png"/></a></h1>
                 <p><span meld:id="version_mid"></span></p>
                 <p>view from <span meld:id="identifier_mid"></span></p>
             </header>
@@ -42,7 +43,7 @@ <h2 meld:id="appli_h_mid">Applications</h2>
 
             <footer>
                 <p>Contact: <a href="mailto:julien.6387.dev@gmail.com">Julien Le Cl&amp;eacute;ach</a></p>
-                <p id="copyright">&amp;copy; Copyright 2016-2022 Julien LE CLEACH</p>
+                <p id="copyright">&amp;copy; Copyright 2016-2023 Julien LE CLEACH</p>
             </footer>
         </div>
 
diff --git a/supvisors/ui/img/icon.png b/supvisors/ui/img/icon.png
new file mode 100644
index 00000000..226c2b69
Binary files /dev/null and b/supvisors/ui/img/icon.png differ
diff --git a/supvisors/ui/img/model/supvisors_icon.xcf b/supvisors/ui/img/model/supvisors_icon.xcf
new file mode 100644
index 00000000..b29f41c1
Binary files /dev/null and b/supvisors/ui/img/model/supvisors_icon.xcf differ
diff --git a/supvisors/ui/img/model/supvisors_logo.xcf b/supvisors/ui/img/model/supvisors_logo.xcf
new file mode 100644
index 00000000..b813c5da
Binary files /dev/null and b/supvisors/ui/img/model/supvisors_logo.xcf differ
diff --git a/supvisors/ui/img/supvisors.png b/supvisors/ui/img/supvisors.png
new file mode 100644
index 00000000..80d1abd2
Binary files /dev/null and b/supvisors/ui/img/supvisors.png differ
diff --git a/supvisors/ui/index.html b/supvisors/ui/index.html
index 35b7e5d5..b4c037ed 100644
--- a/supvisors/ui/index.html
+++ b/supvisors/ui/index.html
@@ -6,6 +6,7 @@
     <meta charset="utf-8" />
     <meta http-equiv="refresh" content="5" meld:id="meta_mid"/>
     <title>Supvisors status</title>
+    <link href="img/icon.png" rel="icon" type="image/png" />
     <link rel="stylesheet" href="css/main_page.css" />
     <link rel="stylesheet" href="css/ui_style.css" />
     <link rel="stylesheet" href="css/menu.css" />
@@ -18,7 +19,9 @@
 
         <div id="left_side">
             <header>
-                <h1><a href="index.html" meld:id="supvisors_mid">Supvisors</a></h1>
+                <h1>
+                    <a href="index.html" meld:id="supvisors_mid"><img src="img/supvisors.png"/></a>
+                </h1>
                 <p><span meld:id="version_mid"></span></p>
                 <p>view from <span meld:id="identifier_mid"></span></p>
             </header>
@@ -43,7 +46,7 @@ <h2 meld:id="appli_h_mid">Applications</h2>
 
             <footer>
                 <p>Contact: <a href="mailto:julien.6387.dev@gmail.com">Julien Le Cl&amp;eacute;ach</a></p>
-                <p id="copyright">&amp;copy; Copyright 2016-2022 Julien LE CLEACH</p>
+                <p id="copyright">&amp;copy; Copyright 2016-2023 Julien LE CLEACH</p>
             </footer>
         </div>
 
diff --git a/supvisors/ui/proc_instance.html b/supvisors/ui/proc_instance.html
index 0df8cfba..dccc068c 100644
--- a/supvisors/ui/proc_instance.html
+++ b/supvisors/ui/proc_instance.html
@@ -6,6 +6,7 @@
     <meta charset="utf-8" />
     <meta http-equiv="refresh" content="5" meld:id="meta_mid"/>
     <title>Supvisors status</title>
+    <link href="img/icon.png" rel="icon" type="image/png" />
     <link rel="stylesheet" href="css/ui_style.css" />
     <link rel="stylesheet" href="css/menu.css" />
     <link rel="stylesheet" href="css/button.css" />
@@ -17,7 +18,7 @@
 
         <div id="left_side">
             <header>
-                <h1><a href="index.html" meld:id="supvisors_mid">Supvisors</a></h1>
+                <h1><a href="index.html" meld:id="supvisors_mid"><img src="img/supvisors.png"/></a></h1>
                 <p><span meld:id="version_mid"></span></p>
                 <p>view from <span meld:id="identifier_mid"></span></p>
             </header>
@@ -42,7 +43,7 @@ <h2 meld:id="appli_h_mid">Applications</h2>
 
             <footer>
                 <p>Contact: <a href="mailto:julien.6387.dev@gmail.com">Julien Le Cl&amp;eacute;ach</a></p>
-                <p id="copyright">&amp;copy; Copyright 2016-2022 Julien LE CLEACH</p>
+                <p id="copyright">&amp;copy; Copyright 2016-2023 Julien LE CLEACH</p>
             </footer>
         </div>
 
@@ -176,42 +177,55 @@ <h3>
                 <div id="process_right_side" meld:id="pstats_div_mid" class="vertical_contents">
                     <h4>Detailed Statistics -- <span meld:id="process_h_mid">*</span></h4>
 
-                    <div>
-                        <table>
-                            <tr>
-                                <th colspan="4">MEM</th>
-                                <th colspan="4">CPU</th>
-                            </tr>
-                            <tr>
-                                <th>%</th>
-                                <th>Mean %</th>
-                                <th>Slope %</th>
-                                <th>SD %</th>
-                                <th>%</th>
-                                <th>Mean %</th>
-                                <th>Slope %</th>
-                                <th>SD %</th>
-                            </tr>
-                            <tr class="brightened">
-                                <td meld:id="pmemval_td_mid">--</td>
-                                <td meld:id="pmemavg_td_mid">--</td>
-                                <td meld:id="pmemslope_td_mid">--</td>
-                                <td meld:id="pmemdev_td_mid">--</td>
-                                <td meld:id="pcpuval_td_mid">--</td>
-                                <td meld:id="pcpuavg_td_mid">--</td>
-                                <td meld:id="pcpuslope_td_mid">--</td>
-                                <td meld:id="pcpudev_td_mid">--</td>
-                            </tr>
-                        </table>
+                    <div class="stats_contents">
+                        <div>
+                            <table>
+                                <tr>
+                                    <th colspan="4">CPU</th>
+                                </tr>
+                                <tr>
+                                    <th>%</th>
+                                    <th>Mean %</th>
+                                    <th>Slope %</th>
+                                    <th>SD %</th>
+                                </tr>
+                                <tr class="brightened">
+                                    <td meld:id="pcpuval_td_mid">--</td>
+                                    <td meld:id="pcpuavg_td_mid">--</td>
+                                    <td meld:id="pcpuslope_td_mid">--</td>
+                                    <td meld:id="pcpudev_td_mid">--</td>
+                                </tr>
+                            </table>
+                        </div>
+                        <div>
+                            <table>
+                                <tr>
+                                    <th colspan="4">MEM</th>
+                                </tr>
+                                <tr>
+                                    <th>%</th>
+                                    <th>Mean %</th>
+                                    <th>Slope %</th>
+                                    <th>SD %</th>
+                                </tr>
+                                <tr class="brightened">
+                                    <td meld:id="pmemval_td_mid">--</td>
+                                    <td meld:id="pmemavg_td_mid">--</td>
+                                    <td meld:id="pmemslope_td_mid">--</td>
+                                    <td meld:id="pmemdev_td_mid">--</td>
+                                </tr>
+                            </table>
+                        </div>
                     </div>
 
                     <figure>
-                        <img src="process_mem.png" alt="Process Memory Graph"/>
+                        <img src="process_cpu.png" alt="Process CPU Graph"/>
                     </figure>
 
                     <figure>
-                        <img src="process_cpu.png" alt="Process CPU Graph"/>
+                        <img src="process_mem.png" alt="Process Memory Graph"/>
                     </figure>
+
                 </div>
             </div>
 
diff --git a/supvisors/utils.py b/supvisors/utils.py
index 12ba38c7..66ebbccc 100644
--- a/supvisors/utils.py
+++ b/supvisors/utils.py
@@ -18,7 +18,6 @@
 # ======================================================================
 
 import re
-from enum import Enum
 from math import sqrt
 from time import gmtime, localtime, strftime, time
 from typing import Tuple
@@ -66,21 +65,19 @@ class SupervisorServerUrl:
 
     def __init__(self, env):
         """ Parse the Supervisor server URL for later modification. """
-        self.env = env
-        self.parsed_url = urlparse(env['SUPERVISOR_SERVER_URL'])
-        # consider the authentication part (just in case)
+        self.env = env.copy()
+        # get the possible authentication part
+        parsed_url = urlparse(env['SUPERVISOR_SERVER_URL'])
         self.authentication = ''
-        if self.parsed_url.username:
-            self.authentication = f'{self.parsed_url.username}'
-            if self.parsed_url.password:
-                self.authentication += f':{self.parsed_url.password}'
+        if parsed_url.username:
+            self.authentication = parsed_url.username
+            if parsed_url.password:
+                self.authentication += f':{parsed_url.password}'
             self.authentication += '@'
 
-    def update_url(self, hostname: str, port: int = None):
-        """ Update the URL by changing the hostname and optionally the port. """
-        netloc = f'{self.authentication}{hostname}:{port if port else self.parsed_url.port}'
-        self.parsed_url = self.parsed_url._replace(netloc=netloc)
-        self.env['SUPERVISOR_SERVER_URL'] = self.parsed_url.geturl()
+    def update_url(self, hostname: str, port: int):
+        """ Update the URL by changing the hostname and the port. """
+        self.env['SUPERVISOR_SERVER_URL'] = f'http://{self.authentication}{hostname}:{port}'
 
 
 # simple functions
@@ -131,16 +128,8 @@ def get_linear_regression(xdata, ydata):
         return a, b
 
 
-def get_simple_linear_regression(lst):
-    """ Calculate the coefficients of the linear equation corresponding
-    to the linear regression of a series of values. """
-    # in Supvisors, Y data is periodic
-    datasize = len(lst)
-    return get_linear_regression([i for i in range(datasize)], lst)
-
-
 # get statistics from data
-def get_stats(lst):
+def get_stats(xdata, ydata):
     """ Calculate the following statistics from a series of points:
     - the mean value,
     - the instant rate between the two last values,
@@ -148,14 +137,14 @@ def get_stats(lst):
     - the standard deviation. """
     rate, a, b, dev = (None,) * 4
     # calculate mean value
-    avg = mean(lst)
-    if len(lst) > 1:
+    avg = mean(ydata)
+    if len(ydata) > 1:
         # calculate instant rate value between last 2 values
-        rate = srate(lst[-1], lst[-2])
+        rate = srate(ydata[-1], ydata[-2])
         # calculate slope value from linear regression of values
-        a, b = get_simple_linear_regression(lst)
+        a, b = get_linear_regression(xdata, ydata)
         # calculate standard deviation
-        dev = stddev(lst, avg)
+        dev = stddev(ydata, avg)
     return avg, rate, (a, b), dev
 
 
diff --git a/supvisors/version.txt b/supvisors/version.txt
index 699f2853..cf6d375c 100644
--- a/supvisors/version.txt
+++ b/supvisors/version.txt
@@ -1,1 +1,1 @@
-version=0.15
+version=0.16
diff --git a/supvisors/viewapplication.py b/supvisors/viewapplication.py
index 376856bf..9722008a 100644
--- a/supvisors/viewapplication.py
+++ b/supvisors/viewapplication.py
@@ -86,6 +86,10 @@ def write_header(self, root):
             # write actions related to application
             self.write_application_actions(root)
 
+    def write_periods(self, root):
+        """ Write configured periods for statistics. """
+        self.write_periods_availability(root, self.has_process_statistics)
+
     def write_starting_strategy(self, root):
         """ Write applicable starting strategies. """
         # get the current strategy
diff --git a/supvisors/viewcontext.py b/supvisors/viewcontext.py
index 43595ccd..13c472f2 100644
--- a/supvisors/viewcontext.py
+++ b/supvisors/viewcontext.py
@@ -104,7 +104,7 @@ def get_gravity(self):
     def update_period(self) -> None:
         """ Extract period from context. """
         default_value = next(iter(self.supvisors.options.stats_periods))
-        self._update_integer(PERIOD, self.supvisors.options.stats_periods, default_value)
+        self._update_float(PERIOD, self.supvisors.options.stats_periods, default_value)
 
     def update_strategy(self) -> None:
         """ Extract starting strategy from context. """
@@ -150,7 +150,7 @@ def update_namespec(self) -> None:
 
     def update_cpu_id(self) -> None:
         """ Extract CPU id from context. """
-        self._update_integer(CPU, list(range(self.get_nbcores() + 1)))
+        self._update_integer(CPU, list(range(self.get_nb_cores() + 1)))
 
     def update_interface_name(self) -> None:
         """ Extract interface name from context.
@@ -205,7 +205,7 @@ def format_url(self, identifier: str, page: str, **kwargs):
         # build network location if identifier is provided
         if identifier:
             instance = self.supvisors.supvisors_mapper.instances[identifier]
-            netloc = f'http://{quote(instance.host_name)}:{instance.http_port}/'
+            netloc = f'http://{quote(instance.host_id)}:{instance.http_port}/'
         # shex must be reset if the Supvisors instance changes
         local_identifier = not identifier or identifier == self.local_identifier
         # build URL from netloc, page and attributes
@@ -221,17 +221,21 @@ def fire_message(self) -> None:
             location = f'{form[SERVER_URL]}{path_translated}?{self.url_parameters(False, **args)}'
             self.http_context.response[HEADERS][LOCATION] = location
 
-    def get_nbcores(self, identifier: str = None) -> int:
+    def get_nb_cores(self, identifier: str = None) -> int:
         """ Get the number of processors of the host where the Supvisors instance is running. """
         stats_identifier = identifier or self.local_identifier
-        return self.supvisors.statistician.nbcores.get(stats_identifier, 0)
+        # 2 chances to get the value
+        nb_cores = self.supvisors.host_compiler.get_nb_cores(stats_identifier)
+        if not nb_cores:
+            nb_cores = self.supvisors.process_compiler.get_nb_cores(stats_identifier)
+        return nb_cores
 
     def get_instance_stats(self, identifier: str = None):
         """ Get the statistics structure related to the identifier and the period selected.
         If no identifier is specified, local identifier is used. """
         stats_identifier = identifier or self.local_identifier
-        stats_instance = self.supvisors.statistician.data.get(stats_identifier, {})
-        return stats_instance.get(self.parameters.get(PERIOD))
+        period = self.parameters.get(PERIOD)
+        return self.supvisors.host_compiler.get_stats(stats_identifier, period)
 
     def get_process_stats(self, namespec: str, identifier: str = None):
         """ Get the statistics structure related to the process and the period selected.
@@ -240,12 +244,12 @@ def get_process_stats(self, namespec: str, identifier: str = None):
         # use local identifier if not provided
         if not identifier:
             identifier = self.local_identifier
+        # get the number of cores for Solaris mode
+        nb_cores = self.get_nb_cores(identifier)
         # return the process statistics for this process
-        instance_stats = self.get_instance_stats(identifier)
-        nb_cores = self.get_nbcores(identifier)
-        if instance_stats:
-            return nb_cores, instance_stats.find_process_stats(namespec)
-        return nb_cores, None
+        period = self.parameters.get(PERIOD)
+        stats_instance = self.supvisors.process_compiler.get_stats(namespec, identifier, period)
+        return nb_cores, stats_instance
 
     def get_process_status(self, namespec: str = None) -> Optional[ProcessStatus]:
         """ Get the ProcessStatus instance related to the process named namespec.
@@ -260,7 +264,7 @@ def get_process_status(self, namespec: str = None) -> Optional[ProcessStatus]:
     def get_application_shex(self, application_name: str) -> Tuple[bool, str]:
         """ Get the expand / shrink value of the application and the shex string to invert it.
 
-        :param application_name: the name of the application
+        :param application_name: the application name
         :return: the application shex and the inverted shex
         """
         shex = self.parameters[SHRINK_EXPAND]
@@ -306,6 +310,25 @@ def _update_integer(self, param, check_list, default_value=0):
         self.logger.trace(f'ViewContext._update_integer: {param} set to {value}')
         self.parameters[param] = value
 
+    def _update_float(self, param, check_list, default_value=0.0):
+        """ Extract information from context and convert to float based on allowed values in check_list. """
+        value = default_value
+        str_value = self.http_context.form.get(param)
+        if str_value:
+            try:
+                float_value = float(str_value)
+            except ValueError:
+                self.store_message = error_message(f'{param} is not a float: {str_value}')
+            else:
+                # check that float_value is defined in check list
+                if float_value in check_list:
+                    value = float_value
+                else:
+                    self.store_message = error_message(f'Incorrect {param}: {float_value}')
+        # assign value found or default
+        self.logger.trace(f'ViewContext._update_float: {param} set to {value}')
+        self.parameters[param] = value
+
     def _update_boolean(self, param, default_value=False):
         """ Extract information from context and convert to boolean based on allowed values in check_list. """
         value = default_value
diff --git a/supvisors/viewhandler.py b/supvisors/viewhandler.py
index dae35f45..eb307cba 100644
--- a/supvisors/viewhandler.py
+++ b/supvisors/viewhandler.py
@@ -25,7 +25,8 @@
 from supervisor.web import MeldView
 
 from .rpcinterface import API_VERSION
-from .ttypes import SupvisorsInstanceStates, SupvisorsStates, Payload, PayloadList, ProcessHistoryStats
+from .statscompiler import ProcStatisticsInstance
+from .ttypes import (SupvisorsInstanceStates, SupvisorsStates, Payload, PayloadList)
 from .utils import get_stats
 from .viewcontext import *
 from .viewimage import process_cpu_img, process_mem_img
@@ -47,8 +48,10 @@ def __init__(self, context):
         self.sup_ctx = self.supvisors.context
         # keep reference to the local node name
         self.local_identifier = self.supvisors.supvisors_mapper.local_identifier
-        # define statistics visibility
-        self.has_statistics = True
+        # even if there is no local collector, statistics can be available from other Supvisors instances
+        # where a collector is available
+        self.has_host_statistics = True
+        self.has_process_statistics = True
         # init view_ctx (only for tests)
         self.view_ctx = None
 
@@ -98,13 +101,10 @@ def write_common(self, root):
         auto_refresh = self.view_ctx.parameters[AUTO]
         if not auto_refresh:
             root.findmeld('meta_mid').deparent()
-        # configure Supvisors hyperlink
-        elt = root.findmeld('supvisors_mid')
-        url = self.view_ctx.format_url('', SUPVISORS_PAGE)
-        elt.attributes(href=url)
         # blink main title in conciliation state
         if self.supvisors.fsm.state == SupvisorsStates.CONCILIATION and self.sup_ctx.conflicts():
-            update_attrib(elt, 'class', 'blink')
+            elt = root.findmeld('supvisors_mid')
+            update_attrib(elt, 'class', 'failure')
         # set Supvisors version
         root.findmeld('version_mid').content(API_VERSION)
         # set current Supvisors instance identifier
@@ -151,7 +151,7 @@ def write_nav_instances(self, root, identifier):
                 if status.state_modes.starting_jobs or status.state_modes.stopping_jobs:
                     update_attrib(elt, 'class', 'blink')
                 if status.state == SupvisorsInstanceStates.RUNNING:
-                    # go to web page located on the Supvisors instance so as to reuse Supervisor StatusView
+                    # go to web page located on the Supvisors instance to reuse Supervisor StatusView
                     url = self.view_ctx.format_url(item, PROC_INSTANCE_PAGE)
                     elt.attributes(href=url)
                     update_attrib(elt, 'class', 'on')
@@ -204,8 +204,13 @@ def write_header(self, root):
         raise NotImplementedError
 
     def write_periods(self, root):
+        """ Write configured periods for statistics.
+        By default, the periods box is visible. """
+        self.write_periods_availability(root, True)
+
+    def write_periods_availability(self, root, available: bool) -> None:
         """ Write configured periods for statistics. """
-        if self.has_statistics:
+        if available:
             # write the available periods
             mid_elt = root.findmeld('period_li_mid')
             for li_elt, item in mid_elt.repeat(self.supvisors.options.stats_periods):
@@ -216,7 +221,7 @@ def write_periods(self, root):
                 else:
                     url = self.view_ctx.format_url('', self.page_name, **{PERIOD: item})
                     elt.attributes(href=url)
-                elt.content('{}s'.format(item))
+                elt.content(f'{item}s')
         else:
             # hide the Statistics periods box
             root.findmeld('period_div_mid').replace('')
@@ -229,12 +234,12 @@ def write_contents(self, root):
     def write_common_process_cpu(self, tr_elt, info):
         """ Write the CPU part of the common process status.
         Statistics data comes from node. """
-        if self.has_statistics:
-            proc_stats = info['proc_stats']
+        if self.has_process_statistics:
+            proc_stats: ProcStatisticsInstance = info['proc_stats']
             elt = tr_elt.findmeld('pcpu_a_mid')
-            if proc_stats and len(proc_stats[0]) > 0:
+            if proc_stats and len(proc_stats.cpu) > 0:
                 # print last CPU value of process
-                cpuvalue = proc_stats[0][-1]
+                cpuvalue = proc_stats.cpu[-1]
                 if not self.supvisors.options.stats_irix_mode:
                     cpuvalue /= info['nb_cores']
                 if info['namespec']:  # empty for an application info
@@ -259,12 +264,12 @@ def write_common_process_cpu(self, tr_elt, info):
     def write_common_process_mem(self, tr_elt, info: Payload) -> None:
         """ Write the MEM part of the common process status.
         Statistics data comes from node. """
-        if self.has_statistics:
-            proc_stats = info['proc_stats']
+        if self.has_process_statistics:
+            proc_stats: ProcStatisticsInstance = info['proc_stats']
             elt = tr_elt.findmeld('pmem_a_mid')
-            if proc_stats and len(proc_stats[1]) > 0:
+            if proc_stats and len(proc_stats.mem) > 0:
                 # print last MEM value of process
-                memvalue = proc_stats[1][-1]
+                memvalue = proc_stats.mem[-1]
                 if info['namespec']:  # empty for an application info
                     update_attrib(elt, 'class', 'button on')
                     parameters = {PROCESS: info['namespec'], IDENTIFIER: info['identifier']}
@@ -342,7 +347,7 @@ def _write_process_button(self, tr_elt, elt_name: str, identifier: str, page: st
 
     def write_common_process_table(self, table_elt):
         """ Hide MEM+CPU head+foot cells if statistics disabled"""
-        if not self.has_statistics:
+        if not self.has_process_statistics:
             for mid in ['mem_head_th_mid', 'cpu_head_th_mid', 'mem_foot_th_mid', 'cpu_foot_th_mid', 'total_mid']:
                 elt = table_elt.findmeld(mid)
                 if elt is not None:
@@ -404,7 +409,7 @@ def write_common_process_status(self, tr_elt, info: Payload, check_logfiles: boo
         self.write_process_stdout_button(tr_elt, info, check_logfiles)
         self.write_process_stderr_button(tr_elt, info, check_logfiles)
 
-    def write_detailed_process_cpu(self, stats_elt, proc_stats: ProcessHistoryStats, nb_cores: int) -> bool:
+    def write_detailed_process_cpu(self, stats_elt, proc_stats: ProcStatisticsInstance, nb_cores: int) -> bool:
         """ Write the CPU part of the detailed process status.
 
         :param stats_elt: the element from which to search for
@@ -412,19 +417,21 @@ def write_detailed_process_cpu(self, stats_elt, proc_stats: ProcessHistoryStats,
         :param nb_cores: the number of processor cores
         :return: True if process CPU statistics are valid
         """
-        if proc_stats and len(proc_stats[0]) > 0:
+        if proc_stats and len(proc_stats.cpu) > 0:
             # calculate stats
-            avg, rate, (slope, intercept), dev = get_stats(proc_stats[0])
+            avg, rate, (slope, intercept), dev = get_stats(proc_stats.times, proc_stats.cpu)
             # print last CPU value of process
             elt = stats_elt.findmeld('pcpuval_td_mid')
             if rate is not None:
                 self.set_slope_class(elt, rate)
-            cpuvalue = proc_stats[0][-1]
+            cpuvalue = proc_stats.cpu[-1]
             if not self.supvisors.options.stats_irix_mode:
                 cpuvalue /= nb_cores
             elt.content(f'{cpuvalue:.2f}%')
             # set mean value
             elt = stats_elt.findmeld('pcpuavg_td_mid')
+            if not self.supvisors.options.stats_irix_mode:
+                avg /= nb_cores
             elt.content(f'{avg:.2f}%')
             if slope is not None:
                 # set slope value between last 2 values
@@ -436,20 +443,20 @@ def write_detailed_process_cpu(self, stats_elt, proc_stats: ProcessHistoryStats,
                 elt.content(f'{dev:.2f}')
             return True
 
-    def write_detailed_process_mem(self, stats_elt, proc_stats: ProcessHistoryStats) -> bool:
+    def write_detailed_process_mem(self, stats_elt, proc_stats: ProcStatisticsInstance) -> bool:
         """ Write the MEM part of the detailed process status.
 
         :param stats_elt: the element from which to search for
         :param proc_stats: the process statistics
         :return: True if process Memory statistics are valid
         """
-        if proc_stats and len(proc_stats[1]) > 0:
-            avg, rate, (a, b), dev = get_stats(proc_stats[1])
+        if proc_stats and len(proc_stats.mem) > 0:
+            avg, rate, (a, b), dev = get_stats(proc_stats.times, proc_stats.mem)
             # print last MEM value of process
             elt = stats_elt.findmeld('pmemval_td_mid')
             if rate is not None:
                 self.set_slope_class(elt, rate)
-            elt.content(f'{proc_stats[1][-1]:.2f}%')
+            elt.content(f'{proc_stats.mem[-1]:.2f}%')
             # set mean value
             elt = stats_elt.findmeld('pmemavg_td_mid')
             elt.content(f'{avg:.2f}%')
@@ -463,20 +470,23 @@ def write_detailed_process_mem(self, stats_elt, proc_stats: ProcessHistoryStats)
                 elt.content(f'{dev:.2f}')
             return True
 
-    def write_process_plots(self, proc_stats: ProcessHistoryStats, nb_cores: int) -> None:
+    def write_process_plots(self, proc_stats: ProcStatisticsInstance, nb_cores: int) -> None:
         """ Write the CPU / Memory plots (only if matplotlib is installed) """
         try:
             from supvisors.plot import StatisticsPlot
             # build CPU image
             cpu_img = StatisticsPlot(self.logger)
-            cpu_values = proc_stats[0]
-            if not self.supvisors.options.stats_irix_mode:
-                cpu_values = [x / nb_cores for x in cpu_values]
+            if self.supvisors.options.stats_irix_mode:
+                cpu_values = proc_stats.cpu
+            else:
+                cpu_values = [x / nb_cores for x in proc_stats.cpu]
+            cpu_img.add_timeline(proc_stats.times)
             cpu_img.add_plot('CPU', '%', cpu_values)
             cpu_img.export_image(process_cpu_img)
             # build Memory image
             mem_img = StatisticsPlot(self.logger)
-            mem_img.add_plot('MEM', '%', proc_stats[1])
+            mem_img.add_timeline(proc_stats.times)
+            mem_img.add_plot('MEM', '%', proc_stats.mem)
             mem_img.export_image(process_mem_img)
         except ImportError:
             # matplotlib not installed
@@ -490,7 +500,7 @@ def write_process_statistics(self, root, info: Payload) -> None:
         namespec = info.get('namespec', None)
         if namespec:
             # set CPU/MEM statistics
-            proc_stats = info['proc_stats']
+            proc_stats: ProcStatisticsInstance = info['proc_stats']
             done_cpu = self.write_detailed_process_cpu(stats_elt, proc_stats, info['nb_cores'])
             done_mem = self.write_detailed_process_mem(stats_elt, proc_stats)
             if done_cpu or done_mem:
diff --git a/supvisors/viewhostinstance.py b/supvisors/viewhostinstance.py
index 1b75f957..ae61a3ed 100644
--- a/supvisors/viewhostinstance.py
+++ b/supvisors/viewhostinstance.py
@@ -17,6 +17,7 @@
 # limitations under the License.
 # ======================================================================
 
+from .statscompiler import HostStatisticsInstance
 from .utils import get_stats
 from .viewcontext import *
 from .viewimage import host_cpu_img, host_mem_img, host_io_img
@@ -38,16 +39,16 @@ def __init__(self, context):
     def write_contents(self, root):
         """ Rendering of tables and figures for address statistics. """
         # get data from statistics module iaw period selection
-        stats_instance = self.view_ctx.get_instance_stats()
+        stats_instance: HostStatisticsInstance = self.view_ctx.get_instance_stats()
         if stats_instance:
-            self.write_processor_statistics(root, stats_instance.cpu)
-            self.write_memory_statistics(root, stats_instance.mem)
-            self.write_network_statistics(root, stats_instance.io)
+            self.write_processor_statistics(root, stats_instance.cpu, stats_instance.times)
+            self.write_memory_statistics(root, stats_instance.mem, stats_instance.times)
+            self.write_network_statistics(root, stats_instance.io, stats_instance.times)
             # write CPU / Memory / Network plots
             try:
-                self._write_cpu_image(stats_instance.cpu)
-                self._write_mem_image(stats_instance.mem)
-                self._write_io_image(stats_instance.io)
+                self._write_cpu_image(stats_instance.cpu, stats_instance.times)
+                self._write_mem_image(stats_instance.mem, stats_instance.times)
+                self._write_io_image(stats_instance.io, stats_instance.times)
             except ImportError:
                 # matplotlib not installed
                 pass
@@ -63,27 +64,27 @@ def _write_processor_single_title(self, tr_elt, selected_cpu_id, cpu_id):
         cpu_id_string = self.view_ctx.cpu_id_to_string(cpu_id)
         elt.content('cpu#%s' % cpu_id_string)
 
-    def _write_processor_single_statistics(self, tr_elt, single_cpu_stats):
+    def _write_processor_single_statistics(self, tr_elt, single_cpu_stats, timeline):
         """ Rendering of the processor statistics for a single core. """
-        self._write_common_statistics(tr_elt, single_cpu_stats,
+        self._write_common_statistics(tr_elt, single_cpu_stats, timeline,
                                       'cpuval_td_mid', 'cpuavg_td_mid',
                                       'cpuslope_td_mid', 'cpudev_td_mid')
 
-    def write_processor_statistics(self, root, cpu_stats):
+    def write_processor_statistics(self, root, cpu_stats, timeline):
         """ Rendering of the processor statistics. """
         selected_cpu_id = self.view_ctx.parameters[CPU]
         iterator = root.findmeld('cpu_tr_mid').repeat(cpu_stats)
         shaded_tr = False
         for cpu_id, (tr_elt, single_cpu_stats) in enumerate(iterator):
             self._write_processor_single_title(tr_elt, selected_cpu_id, cpu_id)
-            self._write_processor_single_statistics(tr_elt, single_cpu_stats)
+            self._write_processor_single_statistics(tr_elt, single_cpu_stats, timeline)
             # set row background and invert
             apply_shade(tr_elt, shaded_tr)
             shaded_tr = not shaded_tr
 
-    def write_memory_statistics(self, root, mem_stats):
+    def write_memory_statistics(self, root, mem_stats, timeline):
         """ Rendering of the memory statistics. """
-        self._write_common_statistics(root, mem_stats,
+        self._write_common_statistics(root, mem_stats, timeline,
                                       'memval_td_mid', 'memavg_td_mid',
                                       'memslope_td_mid', 'memdev_td_mid')
 
@@ -106,16 +107,16 @@ def _write_network_single_title(self, tr_elt, selected_intf, intf, rowspan, shad
         else:
             elt.replace('')
 
-    def _write_network_single_statistics(self, tr_elt, single_io_stats, rowspan):
+    def _write_network_single_statistics(self, tr_elt, single_io_stats, timeline, rowspan):
         """ Rendering of the statistics columns of the network statistics. """
         elt = tr_elt.findmeld('intfrxtx_td_mid')
         elt.content('Rx' if rowspan else 'Tx')
         # calculate and write statistics
-        self._write_common_statistics(tr_elt, single_io_stats,
+        self._write_common_statistics(tr_elt, single_io_stats, timeline,
                                       'intfval_td_mid', 'intfavg_td_mid',
                                       'intfslope_td_mid', 'intfdev_td_mid')
 
-    def write_network_statistics(self, root, io_stats):
+    def write_network_statistics(self, root, io_stats, timeline):
         """ Rendering of the network statistics. """
         selected_intf = self.view_ctx.parameters[INTF]
         # display io statistics
@@ -130,16 +131,16 @@ def write_network_statistics(self, root, io_stats):
             # set interface cell rowspan
             self._write_network_single_title(tr_elt, selected_intf, intf, rowspan, shaded_tr)
             # set interface direction
-            self._write_network_single_statistics(tr_elt, single_io_stats, rowspan)
+            self._write_network_single_statistics(tr_elt, single_io_stats, timeline, rowspan)
             if not rowspan:
                 shaded_tr = not shaded_tr
             rowspan = not rowspan
 
-    def _write_common_statistics(self, root, stats, val_mid, avg_mid, slope_mid, dev_mid):
+    def _write_common_statistics(self, root, stats, timeline, val_mid, avg_mid, slope_mid, dev_mid):
         """ Rendering of the memory statistics. """
         if len(stats) > 0:
             # get additional statistics
-            avg, rate, (a, b), dev = get_stats(stats)
+            avg, rate, (a, b), dev = get_stats(timeline, stats)
             # set last value
             elt = root.findmeld(val_mid)
             if rate is not None:
@@ -157,7 +158,7 @@ def _write_common_statistics(self, root, stats, val_mid, avg_mid, slope_mid, dev
                 elt = root.findmeld(dev_mid)
                 elt.content(f'{dev:.2f}')
 
-    def _write_cpu_image(self, cpu_stats):
+    def _write_cpu_image(self, cpu_stats, timeline):
         """ Write CPU data into the dedicated buffer. """
         from supvisors.plot import StatisticsPlot
         # get CPU data
@@ -166,18 +167,20 @@ def _write_cpu_image(self, cpu_stats):
         cpu_data = cpu_stats[cpu_id]
         # build image from data
         plt = StatisticsPlot(self.logger)
+        plt.add_timeline(timeline)
         plt.add_plot(f'CPU #{cpu_id_string}', '%', cpu_data)
         plt.export_image(host_cpu_img)
 
-    def _write_mem_image(self, mem_stats):
+    def _write_mem_image(self, mem_stats, timeline):
         """ Write MEM data into the dedicated buffer. """
         # build image from data
         from supvisors.plot import StatisticsPlot
         plt = StatisticsPlot(self.logger)
+        plt.add_timeline(timeline)
         plt.add_plot('MEM', '%', mem_stats)
         plt.export_image(host_mem_img)
 
-    def _write_io_image(self, io_stats):
+    def _write_io_image(self, io_stats, timeline):
         """ Write MEM data into the dedicated buffer. """
         from supvisors.plot import StatisticsPlot
         # get IO data
@@ -187,6 +190,7 @@ def _write_io_image(self, io_stats):
             sent_data = io_stats[intf_name][1]
             # build image from data
             plt = StatisticsPlot(self.logger)
+            plt.add_timeline(timeline)
             plt.add_plot(f'{intf_name} recv', 'kbits/s', recv_data)
             plt.add_plot(f'{intf_name} sent', 'kbits/s', sent_data)
             plt.export_image(host_io_img)
diff --git a/supvisors/viewinstance.py b/supvisors/viewinstance.py
index f33dd2aa..727010d5 100644
--- a/supvisors/viewinstance.py
+++ b/supvisors/viewinstance.py
@@ -37,7 +37,9 @@ def __init__(self, context, page_name):
         """ Call of the superclass constructors. """
         StatusView.__init__(self, context)
         self.page_name = page_name
-        self.has_statistics = self.supvisors.listener.collector and self.supvisors.options.stats_enabled
+        # this class deals with local statistics so a local collector must be available
+        self.has_host_statistics = self.supvisors.host_collector is not None
+        self.has_process_statistics = self.supvisors.process_collector is not None
 
     def render(self):
         """ Catch render to force the use of ViewHandler's method instead of StatusView's method. """
@@ -96,7 +98,7 @@ def write_instance_actions(self, root, status: SupvisorsInstanceStatus):
 
     def write_view_switch(self, root, status: SupvisorsInstanceStatus):
         """ Write actions related to the Supvisors instance. """
-        if self.has_statistics:
+        if self.has_host_statistics:
             # update process button
             if self.page_name == HOST_INSTANCE_PAGE:
                 elt = root.findmeld('process_view_a_mid')
@@ -104,7 +106,7 @@ def write_view_switch(self, root, status: SupvisorsInstanceStatus):
                 elt.attributes(href=url)
             # update host button
             elt = root.findmeld('host_view_a_mid')
-            elt.content(f'{status.supvisors_id.host_name}')
+            elt.content(f'{status.supvisors_id.host_id}')
             if self.page_name == PROC_INSTANCE_PAGE:
                 url = self.view_ctx.format_url('', HOST_INSTANCE_PAGE)
                 elt.attributes(href=url)
diff --git a/supvisors/viewprocinstance.py b/supvisors/viewprocinstance.py
index 8c4c025d..6e608d93 100644
--- a/supvisors/viewprocinstance.py
+++ b/supvisors/viewprocinstance.py
@@ -23,7 +23,8 @@
 
 from .application import ApplicationStatus
 from .instancestatus import SupvisorsInstanceStatus
-from .ttypes import SupvisorsFaults, Payload, PayloadList, ProcessHistoryStats
+from .statscompiler import ProcStatisticsInstance
+from .ttypes import SupvisorsFaults, Payload, PayloadList, ProcessCPUHistoryStats, ProcessMemHistoryStats
 from .viewcontext import *
 from .viewinstance import SupvisorsInstanceView
 from .webutils import *
@@ -40,6 +41,10 @@ def __init__(self, context):
         """ Call of the superclass constructors. """
         SupvisorsInstanceView.__init__(self, context, PROC_INSTANCE_PAGE)
 
+    def write_periods(self, root):
+        """ Write configured periods for statistics. """
+        self.write_periods_availability(root, self.has_process_statistics)
+
     # RIGHT SIDE / BODY part
     def write_contents(self, root):
         """ Rendering of the contents part of the page. """
@@ -75,7 +80,7 @@ def get_process_data(self) -> Tuple[PayloadList, PayloadList]:
             # a 'single' process has the same name as its application and the application contains only one process
             single = process.process_name == process.application_name and len(application.processes) == 1
             info = process.info_map[self.view_ctx.local_identifier]
-            crashed = ProcessStatus.is_crashed_event(info['state'], info['expected'])
+            crashed = ProcessStatus.is_crashed_event(info)
             nb_cores, proc_stats = self.view_ctx.get_process_stats(namespec)
             payload = {'application_name': info['group'], 'process_name': info['name'], 'namespec': namespec,
                        'single': single, 'identifier': self.view_ctx.local_identifier,
@@ -158,7 +163,7 @@ def get_application_summary(self, application_name: str, application_processes:
         return payload
 
     @staticmethod
-    def sum_process_info(data: PayloadList) -> Tuple[int, int, Optional[ProcessHistoryStats]]:
+    def sum_process_info(data: PayloadList) -> Tuple[int, int, Optional[Tuple[ProcessCPUHistoryStats, ProcessMemHistoryStats]]]:
         """ Get the total resources taken by the processes.
 
         :param data: the list of process payloads
@@ -173,14 +178,18 @@ def sum_process_info(data: PayloadList) -> Tuple[int, int, Optional[ProcessHisto
                 # sum CPU / Mem stats
                 proc_stats = info['proc_stats']
                 if proc_stats:
-                    if len(proc_stats[0]) and len(proc_stats[1]):
+                    if len(proc_stats.cpu) and len(proc_stats.mem):
                         reset = False
                         # the most recent value is at the end of the list
-                        cpu += proc_stats[0][-1]
-                        mem += proc_stats[1][-1]
+                        cpu += proc_stats.cpu[-1]
+                        mem += proc_stats.mem[-1]
         # reset appli_stats if no process involved
         # keep output similar to process stats
-        appli_stats = None if reset else [[cpu], [mem]]
+        appli_stats = None
+        if not reset:
+            appli_stats = ProcStatisticsInstance()
+            appli_stats.cpu = [cpu]
+            appli_stats.mem = [mem]
         return expected_load, nb_cores, appli_stats
 
     def write_process_table(self, root, sorted_data: PayloadList, excluded_data: PayloadList) -> None:
@@ -352,13 +361,13 @@ def write_total_status(self, table_elt, sorted_data: PayloadList, excluded_data:
             if appli_stats:
                 # update MEM
                 elt = tr_elt.findmeld('mem_total_th_mid')
-                elt.content(f'{appli_stats[1][0]:.2f}%')
+                elt.content(f'{appli_stats.mem[0]:.2f}%')
                 # update CPU
                 elt = tr_elt.findmeld('cpu_total_th_mid')
-                cpuvalue = appli_stats[0][0]
+                cpu_value = appli_stats.cpu[0]
                 if not self.supvisors.options.stats_irix_mode:
-                    cpuvalue /= nb_cores
-                elt.content(f'{cpuvalue:.2f}%')
+                    cpu_value /= nb_cores
+                elt.content(f'{cpu_value:.2f}%')
 
     # ACTION part
     def make_callback(self, namespec, action):
diff --git a/supvisors/webutils.py b/supvisors/webutils.py
index 9499f205..9162d5a3 100644
--- a/supvisors/webutils.py
+++ b/supvisors/webutils.py
@@ -162,7 +162,7 @@ def update_attrib(elt, attribute: str, value: str) -> None:
     """
     current_value = elt.attrib.get(attribute, '')
     if value not in current_value:
-        elt.attrib[attribute] = '{} {}'.format(current_value, value).strip()
+        elt.attrib[attribute] = f'{current_value} {value}'.strip()
 
 
 def apply_shade(elt, shaded: bool) -> None:
diff --git a/tox.ini b/tox.ini
index 87df46fb..055bdf16 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,9 @@
 [tox]
 envlist =
-    cover,py36,py37,py38,py39,docs
+    cover,py36,py37,py38,py39,py310,docs
 
-[testenv]
-commands =
-    pytest
+[testenv:py36]
+basepython = python3.6
 deps =
     supervisor >= 4.2.1
     pyzmq >= 20.0.0
@@ -16,22 +15,32 @@ deps =
     pyparsing >= 2.0.2, < 3
     pytest
     pytest-mock
+commands =
+    pytest
+
+
+[testenv]
+deps =
+    {[testenv:py36]deps}
+    websockets == 10.2
+commands = {[testenv:py36]commands}
 
 
 [testenv:cover]
-commands =
-    pytest --cov=supvisors --cov-report=xml --cov-report=term-missing
 deps =
     {[testenv]deps}
     pytest-cov
+commands =
+    pytest --cov=supvisors --cov-report=xml --cov-report=term-missing
 
 
 [testenv:docs]
 deps =
     pyzmq >= 20.0.0
+    websockets >= 10.2
     sphinx == 4.1.2
     renku-sphinx-theme == 0.2.2
     myst-parser == 0.15.2
-whitelist_externals = make
+allowlist_externals = make
 commands =
     make -C docs html BUILDDIR={envtmpdir} "SPHINXOPTS=-W -E"


Test patch:
diff --git a/supvisors/test/etc/rocky51/service/program_flask.ini b/supvisors/test/etc/rocky51/service/program_flask.ini
index b567e3c1..6845cb48 100644
--- a/supvisors/test/etc/rocky51/service/program_flask.ini
+++ b/supvisors/test/etc/rocky51/service/program_flask.ini
@@ -1,5 +1,5 @@
 [program:supvisorsflask]
-command=supvisorsflask
+command=supvisorsflask -u http://localhost:60000
 autostart=true
 autorestart=unexpected
 startsecs=2
diff --git a/supvisors/test/etc/supervisord.conf b/supvisors/test/etc/supervisord.conf
index 18ec20c5..7b9acdd6 100644
--- a/supvisors/test/etc/supervisord.conf
+++ b/supvisors/test/etc/supervisord.conf
@@ -1,3 +1,6 @@
+[unix_http_server]
+file=/tmp/supervisor.sock
+
 [inet_http_server]
 port=:60000
 ;username=cliche
@@ -7,6 +10,7 @@ port=:60000
 logfile=./log/supervisord.log
 loglevel=info
 pidfile=/tmp/supervisord.pid
+minfds=1536
 
 [rpcinterface:supervisor]
 supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
@@ -32,11 +36,12 @@ disabilities_file = /tmp/disabilities.json
 starting_strategy = CONFIG
 conciliation_strategy = USER
 stats_enabled = true
-stats_periods = 5,60,600
+stats_collecting_period = 1
+stats_periods = 2.5,20,160
 stats_histo = 100
 stats_irix_mode = true
-tail_limit=50MB
-tailf_limit=50MB
+tail_limit = 50MB
+tailf_limit = 50MB
 logfile = AUTO
 ;logfile = ./log/supvisors.log
 ;logfile_maxbytes = 50MB
diff --git a/supvisors/test/scripts/check_conciliation_strategy.py b/supvisors/test/scripts/check_conciliation_strategy.py
index 6cbc4b12..5eb51a9e 100644
--- a/supvisors/test/scripts/check_conciliation_strategy.py
+++ b/supvisors/test/scripts/check_conciliation_strategy.py
@@ -21,12 +21,11 @@
 import time
 import unittest
 
-from socket import gethostname
 from supervisor.compat import xmlrpclib
 from supervisor.states import ProcessStates
 from supervisor.xmlrpc import Faults
-from supvisors.ttypes import ConciliationStrategies, StartingStrategies
 
+from supvisors.ttypes import ConciliationStrategies, StartingStrategies
 from .event_queues import SupvisorsEventQueues
 from .running_identifiers import RunningIdentifiersTest
 
@@ -242,7 +241,6 @@ def _check_no_conflict(self):
         state = self.local_supvisors.get_supvisors_state()
         self.assertEqual('OPERATION', state['fsm_statename'])
         # test that Supvisors conflicts is empty
-        print(self.local_supvisors.get_conflicts())
         self.assertEqual([], self.local_supvisors.get_conflicts())
 
     def _create_database_conflicts(self):
diff --git a/supvisors/test/scripts/check_stop_sequence.py b/supvisors/test/scripts/check_stop_sequence.py
index b236f476..29989d82 100644
--- a/supvisors/test/scripts/check_stop_sequence.py
+++ b/supvisors/test/scripts/check_stop_sequence.py
@@ -116,7 +116,7 @@ def check_unmanaged_stopping(self):
                 for identifier in program.identifiers:
                     program.add_event(ProcessStateEvent(ProcessStates.STOPPING, identifier))
                     program.add_event(ProcessStateEvent(ProcessStates.STOPPED))
-        # configure evtlistener stop sequence
+        # configure evt_listener stop sequence
         program = self.context.get_program('evt_listener:evt_listener_00')
         if program:
             if program.state in RUNNING_STATES:
diff --git a/supvisors/test/scripts/event_queues.py b/supvisors/test/scripts/event_queues.py
index 5c4ea48e..a58bca83 100644
--- a/supvisors/test/scripts/event_queues.py
+++ b/supvisors/test/scripts/event_queues.py
@@ -17,8 +17,8 @@
 # limitations under the License.
 # ======================================================================
 
-from time import time
 from queue import Empty, Queue
+from time import time
 
 from supvisors.client.zmqsubscriber import SupvisorsZmqEventInterface
 
@@ -33,8 +33,10 @@ class SupvisorsEventQueues(SupvisorsZmqEventInterface):
     def __init__(self, zcontext, logger):
         """ Initialization of the attributes. """
         # create logger using a BoundIO
-        SupvisorsZmqEventInterface.__init__(self, zcontext, self.PORT, logger)
-        self.subscriber.subscribe_all()
+        SupvisorsZmqEventInterface.__init__(self, zcontext, 'localhost', self.PORT, logger)
+        self.subscribe_all()
+        self.unsubscribe_host_statistics()
+        self.unsubscribe_process_statistics()
         # create queues to store messages
         self.supvisors_queue = Queue()
         self.instance_queue = Queue()
diff --git a/supvisors/test/scripts/running_identifiers.py b/supvisors/test/scripts/running_identifiers.py
index cec0807a..a32b65b3 100644
--- a/supvisors/test/scripts/running_identifiers.py
+++ b/supvisors/test/scripts/running_identifiers.py
@@ -19,16 +19,14 @@
 
 import os
 import unittest
+from queue import Empty
 
 import zmq
-
-from queue import Empty
 from supervisor.childutils import getRPCInterface
 
+from supvisors.client.clientutils import create_logger
 from supvisors.ttypes import SupvisorsInstanceStates
 from supvisors.utils import SupervisorServerUrl
-from supvisors.client.zmqsubscriber import create_logger
-
 from .event_queues import SupvisorsEventQueues
 
 
@@ -53,13 +51,13 @@ def setUp(self):
                                     if info['statecode'] == SupvisorsInstanceStates.RUNNING.value}
         self.assertEqual(3, len(self.running_identifiers))
         # keep a reference to all RPC proxies
-        supervisor_url = SupervisorServerUrl(os.environ.copy())
+        supervisor_url = SupervisorServerUrl(os.environ)
         self.proxies = {}
         for identifier, (node_name, port) in self.running_identifiers.items():
             supervisor_url.update_url(node_name, port)
             self.proxies[identifier] = getRPCInterface(supervisor_url.env)
         # create the thread of event subscriber
-        self.zcontext = zmq.Context.instance()
+        self.zcontext = zmq.asyncio.Context.instance()
         self.logger = create_logger(logfile=r'./log/running_identifiers.log')
         self.evloop = SupvisorsEventQueues(self.zcontext, self.logger)
         # start the thread
@@ -69,7 +67,6 @@ def setUp(self):
     def tearDown(self):
         """ The tearDown stops the subscriber to the Supvisors events. """
         self.evloop.stop()
-        self.evloop.join()
         self.logger.info('Event loop ended')
         # close resources
         self.logger.close()
diff --git a/supvisors/test/scripts/sequence_checker.py b/supvisors/test/scripts/sequence_checker.py
index e012edcd..8f82e659 100644
--- a/supvisors/test/scripts/sequence_checker.py
+++ b/supvisors/test/scripts/sequence_checker.py
@@ -18,17 +18,15 @@
 # ======================================================================
 
 import unittest
-import zmq
-
 from queue import Empty
 from typing import Dict
 
+import zmq.asyncio
 from supervisor.options import split_namespec
 from supervisor.states import ProcessStates, getProcessStateDescription, RUNNING_STATES, STOPPED_STATES
 
-from supvisors.client.zmqsubscriber import create_logger
+from supvisors.client.clientutils import create_logger
 from supvisors.ttypes import ApplicationStates
-
 from .event_queues import SupvisorsEventQueues
 
 
@@ -205,7 +203,7 @@ def __init__(self, zcontext, logger):
 
     def configure(self):
         """ Subscribe to Supvisors instances status only. """
-        self.subscriber.subscribe_instance_status()
+        self.subscribe_instance_status()
 
     def on_instance_status(self, data):
         """ Pushes the Supvisors Instance Status message into a queue. """
@@ -217,10 +215,10 @@ def on_instance_status(self, data):
         if self.nb_identifiers_notifications == 7:
             self.logger.info(f'instances: {self.identifiers}')
             # got all notification, unsubscribe from SupvisorsInstanceStatus
-            self.subscriber.unsubscribe_instance_status()
+            self.unsubscribe_instance_status()
             # subscribe to application and process status
-            self.subscriber.subscribe_application_status()
-            self.subscriber.subscribe_process_status()
+            self.subscribe_application_status()
+            self.subscribe_process_status()
             # notify CheckSequence with an event in start_queue
             self.instance_queue.put(self.identifiers)
 
@@ -238,7 +236,7 @@ def setUp(self):
         # create a context
         self.context = Context()
         # create the thread of event subscriber
-        self.zcontext = zmq.Context.instance()
+        self.zcontext = zmq.asyncio.Context.instance()
         self.logger = create_logger(logfile=r'./log/check_sequence.log')
         self.evloop = SequenceChecker(self.zcontext, self.logger)
         self.evloop.start()
@@ -246,7 +244,6 @@ def setUp(self):
     def tearDown(self):
         """ The tearDown stops the subscriber to the Supvisors events. """
         self.evloop.stop()
-        self.evloop.join()
         # close resources
         self.logger.close()
         self.zcontext.term()
@@ -309,11 +306,11 @@ def check_process_event(self, event):
 
     def check_application_event(self, event):
         """ Check if the received application event corresponds to expectation. """
-        self.evloop.logger.info(f'Checking application event: {event}')
         # check that event corresponds to an expected application
         application_name = event['application_name']
         application = self.context.get_application(application_name)
         self.assertIsNotNone(application)
+        self.evloop.logger.info(f'Checking application event={event} against {application.__dict__}')
         # check event contents in accordance with context
         if application.is_starting():
             self.assertDictContainsSubset({'statename': 'STARTING', 'statecode': ApplicationStates.STARTING.value},
diff --git a/supvisors/tests/base.py b/supvisors/tests/base.py
index 280ee6ea..28a9104b 100644
--- a/supvisors/tests/base.py
+++ b/supvisors/tests/base.py
@@ -20,7 +20,7 @@
 import os
 import random
 import socket
-from socket import gethostname
+from socket import getfqdn, gethostname
 from unittest.mock import Mock
 
 from supervisor.loggers import getLogger, handle_stdout, Logger
@@ -31,6 +31,8 @@
 from supvisors.context import Context
 from supvisors.initializer import Supvisors
 from supvisors.rpcinterface import RPCInterface
+from supvisors.statscollector import ProcessStatisticsCollector, instant_host_statistics
+from supvisors.statscompiler import HostStatisticsCompiler, ProcStatisticsCompiler
 from supvisors.supervisordata import SupervisorData
 from supvisors.supvisorsmapper import SupvisorsMapper
 from supvisors.supvisorssocket import SupvisorsSockets
@@ -50,10 +52,16 @@ def __init__(self, supervisord, config):
         self.supervisor_data = SupervisorData(self, supervisord)
         self.supvisors_mapper = SupvisorsMapper(self)
         host_name = gethostname()
+        fqdn = getfqdn()
         identifiers = ['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4', '10.0.0.5',
-                       f'<{host_name}>{host_name}:65000:', f'<test>{host_name}:55000:55100']
+                       f'<{host_name}>{fqdn}:65000:', f'<test>{fqdn}:55000:55100']
         self.supvisors_mapper.configure(identifiers, [])
         self.server_options = Mock(procnumbers={'xclock': 2})
+        # set real statistics collectors
+        self.host_collector = instant_host_statistics
+        self.process_collector = ProcessStatisticsCollector(self.logger)
+        self.host_compiler = HostStatisticsCompiler(self)
+        self.process_compiler = ProcStatisticsCompiler(self.options, self.logger)
         # build context from node mapper
         self.context = Context(self)
         # mock by spec
@@ -66,7 +74,6 @@ def __init__(self, supervisord, config):
         self.stopper = Mock(spec=Stopper)
         self.failure_handler = Mock(spec=RunningFailureHandler)
         self.fsm = Mock(spec=FiniteStateMachine)
-        self.statistician = Mock(data={}, nbcores={})
         self.listener = Mock(spec=SupervisorListener, collector=Mock())
         self.parser = Mock(spec=Parser)
         # should be set in listener
@@ -110,6 +117,7 @@ def __init__(self):
         # build a fake server config
         server_config = {'section': 'inet_http_server',
                          'family': socket.AF_INET,
+                         'host': gethostname(),
                          'port': 65000,
                          'username': 'user',
                          'password': 'p@$$w0rd'}
@@ -119,7 +127,7 @@ def __init__(self):
         self.here = '.'
         self.environ_expansions = {}
         self.identifier = gethostname()
-        self.serverurl = f'http://{gethostname()}:65000'
+        self.serverurl = 'unix://tmp/supervisor.sock'
         self.mood = SupervisorStates.RUNNING
         self.nodaemon = True
         self.silent = False
@@ -195,6 +203,7 @@ def __init__(self, template=None):
                      'processname': 'dummy_proc',
                      'appliname': 'dummy_appli',
                      'intfname': 'eth0',
+                     'period': 5.1,
                      'auto': 'false'}
         self.response = {'headers': {'Location': None}}
 
diff --git a/supvisors/tests/conftest.py b/supvisors/tests/conftest.py
index f6b86934..575a5dcb 100644
--- a/supvisors/tests/conftest.py
+++ b/supvisors/tests/conftest.py
@@ -73,7 +73,7 @@ def supervisor():
 
 @pytest.fixture
 def supvisors(mocker, supervisor, options):
-    mocker.patch('supvisors.supvisorsmapper.get_node_names', side_effect=lambda x, y: [x])
+    mocker.patch('supvisors.supvisorsmapper.get_addresses', side_effect=lambda x, y: (x, [], [x]))
     return MockedSupvisors(supervisor, options)
 
 
diff --git a/supvisors/tests/test_clientutils.py b/supvisors/tests/test_clientutils.py
new file mode 100644
index 00000000..8ec62deb
--- /dev/null
+++ b/supvisors/tests/test_clientutils.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# ======================================================================
+# Copyright 2022 Julien LE CLEACH
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ======================================================================
+
+
+from unittest.mock import call
+
+from supervisor.loggers import Logger, LevelsByName
+
+from supvisors.client.clientutils import create_logger
+
+
+def test_create_logger(mocker):
+    """ Test the create_logger method. """
+    mocked_stdout = mocker.patch('supervisor.loggers.handle_stdout')
+    mocked_file = mocker.patch('supervisor.loggers.handle_file')
+    # test default
+    default_format = '%(asctime)s;%(levelname)s;%(message)s\n'
+    logger = create_logger()
+    assert isinstance(logger, Logger)
+    assert logger.level == LevelsByName.INFO
+    assert mocked_stdout.call_args_list == [call(logger, default_format)]
+    assert mocked_file.call_args_list == [call(logger, r'subscriber.log', default_format, True, 10485760, 1)]
+    mocker.resetall()
+    # test with parameters
+    new_format = '%(asctime)s %(message)s'
+    logger = create_logger('/tmp/client.log', LevelsByName.CRIT, new_format, False, 1024, 10, False)
+    assert isinstance(logger, Logger)
+    assert logger.level == LevelsByName.CRIT
+    assert not mocked_stdout.called
+    assert mocked_file.call_args_list == [call(logger, '/tmp/client.log', new_format, False, 1024, 10)]
diff --git a/supvisors/tests/test_commander.py b/supvisors/tests/test_commander.py
index 3bf9d983..b725a134 100644
--- a/supvisors/tests/test_commander.py
+++ b/supvisors/tests/test_commander.py
@@ -759,7 +759,7 @@ def test_application_start_job_distribute_to_single_node(mocker, supvisors, appl
                for command in sequence)
     mocker.resetall()
     # test resource found
-    mocked_get_node.return_value = supvisors.supvisors_mapper.instances['test'].host_name
+    mocked_get_node.return_value = supvisors.supvisors_mapper.instances['test'].host_id
     mocked_get_instance.return_value = '10.0.0.1'
     application_start_job_1.distribute_to_single_node()
     expected_identifiers = [supvisors.supvisors_mapper.local_identifier, 'test']
diff --git a/supvisors/tests/test_context.py b/supvisors/tests/test_context.py
index a601e79e..26100bae 100644
--- a/supvisors/tests/test_context.py
+++ b/supvisors/tests/test_context.py
@@ -24,7 +24,7 @@
 import pytest
 
 from supvisors.context import *
-from supvisors.publisherinterface import EventPublisherInterface
+from supvisors.eventinterface import EventPublisherInterface
 from supvisors.ttypes import SupvisorsInstanceStates, ApplicationStates, SupvisorsStates
 from .base import database_copy, any_process_info
 from .conftest import create_application, create_process
@@ -178,14 +178,15 @@ def test_get_nodes_load(mocker, context):
     local_identifier = context.supvisors.supvisors_mapper.local_identifier
     local_instance = context.supvisors.supvisors_mapper.instances[local_identifier]
     # empty test
+    print(local_instance.host_id)
     assert context.get_nodes_load() == {'10.0.0.1': 0, '10.0.0.2': 0, '10.0.0.3': 0, '10.0.0.4': 0, '10.0.0.5': 0,
-                                        local_instance.host_name: 0}
+                                        local_instance.host_id: 0}
     # update context for some values
     mocker.patch.object(context.instances[local_identifier], 'get_load', return_value=10)
     mocker.patch.object(context.instances['10.0.0.2'], 'get_load', return_value=8)
     mocker.patch.object(context.instances['test'], 'get_load', return_value=5)
     assert context.get_nodes_load() == {'10.0.0.1': 0, '10.0.0.2': 8, '10.0.0.3': 0, '10.0.0.4': 0, '10.0.0.5': 0,
-                                        local_identifier: 15}
+                                        local_instance.ip_address: 15}
 
 
 def test_instances_by_states(context):
@@ -651,7 +652,8 @@ def test_on_tick_event(mocker, context):
         assert status.remote_time == 1234
         assert mocked_check.call_args_list == [call('10.0.0.1')]
         expected = {'identifier': '10.0.0.1', 'node_name': '10.0.0.1', 'port': 65000, 'sequence_counter': 31,
-                    'statecode': 1, 'statename': 'CHECKING', 'remote_time': 1234, 'local_time': 3600, 'loading': 0,
+                    'statecode': 1, 'statename': 'CHECKING', 'remote_time': 1234, 'local_time': 3600,
+                    'loading': 0, 'process_failure': False,
                     'fsm_statecode': 0, 'fsm_statename': 'OFF', 'starting_jobs': False, 'stopping_jobs': False}
         assert mocked_send.call_args_list == [call(expected)]
         mocked_check.reset_mock()
@@ -665,8 +667,8 @@ def test_on_tick_event(mocker, context):
         assert not mocked_check.called
         expected = {'identifier': '10.0.0.1', 'node_name': '10.0.0.1', 'port': 65000, 'sequence_counter': 57,
                     'statecode': state.value, 'statename': state.name, 'remote_time': 5678, 'local_time': 3600,
-                    'loading': 0, 'fsm_statecode': 0, 'fsm_statename': 'OFF', 'starting_jobs': False,
-                    'stopping_jobs': False}
+                    'loading': 0, 'process_failure': False,
+                    'fsm_statecode': 0, 'fsm_statename': 'OFF', 'starting_jobs': False, 'stopping_jobs': False}
         assert mocked_send.call_args_list == [call(expected)]
         mocked_send.reset_mock()
     # check that the node local_sequence_counter is forced to 0 when its sequence_counter is lower than expected
@@ -1246,10 +1248,12 @@ def test_on_timer_event(mocker, context):
     assert context.local_sequence_counter == 32
     assert context.instances['10.0.0.5'].state == SupvisorsInstanceStates.ISOLATING
     expected_1 = {'identifier': '10.0.0.2', 'node_name': '10.0.0.2', 'port': 65000, 'statecode': 4,
-                  'statename': 'ISOLATING', 'remote_time': 0, 'local_time': 0, 'loading': 15, 'sequence_counter': 0,
+                  'statename': 'ISOLATING', 'remote_time': 0, 'local_time': 0, 'loading': 15,
+                  'sequence_counter': 0, 'process_failure': False,
                   'fsm_statecode': 0, 'fsm_statename': 'OFF', 'starting_jobs': False, 'stopping_jobs': False}
     expected_2 = {'identifier': '10.0.0.5', 'node_name': '10.0.0.5', 'port': 65000, 'statecode': 4,
-                  'statename': 'ISOLATING', 'remote_time': 0, 'local_time': 0, 'loading': 0, 'sequence_counter': 0,
+                  'statename': 'ISOLATING', 'remote_time': 0, 'local_time': 0, 'loading': 0,
+                  'sequence_counter': 0, 'process_failure': False,
                   'fsm_statecode': 0, 'fsm_statename': 'OFF', 'starting_jobs': False, 'stopping_jobs': False}
     assert mocked_send.call_args_list == [call(expected_1), call(expected_2)]
     assert proc_2.invalidate_identifier.call_args_list == [call('10.0.0.2')]
@@ -1287,10 +1291,12 @@ def test_handle_isolation(context):
     assert context.instances['10.0.0.5'].state == SupvisorsInstanceStates.ISOLATED
     # check calls to publisher.send_instance_status
     expected_1 = {'identifier': '10.0.0.4', 'node_name': '10.0.0.4', 'port': 65000, 'statecode': 5,
-                  'statename': 'ISOLATED', 'remote_time': 0, 'local_time': 0, 'loading': 0, 'sequence_counter': 0,
+                  'statename': 'ISOLATED', 'remote_time': 0, 'local_time': 0, 'loading': 0,
+                  'sequence_counter': 0, 'process_failure': False,
                   'fsm_statecode': 0, 'fsm_statename': 'OFF', 'starting_jobs': False, 'stopping_jobs': False}
     expected_2 = {'identifier': '10.0.0.5', 'node_name': '10.0.0.5', 'port': 65000, 'statecode': 5,
-                  'statename': 'ISOLATED', 'remote_time': 0, 'local_time': 0, 'loading': 0, 'sequence_counter': 0,
+                  'statename': 'ISOLATED', 'remote_time': 0, 'local_time': 0, 'loading': 0,
+                  'sequence_counter': 0, 'process_failure': False,
                   'fsm_statecode': 0, 'fsm_statename': 'OFF', 'starting_jobs': False, 'stopping_jobs': False}
     assert mocked_send_instance.call_args_list == [call(expected_1), call(expected_2)]
     # no change in state and modes. only one publication expected
diff --git a/supvisors/tests/test_eventinterface.py b/supvisors/tests/test_eventinterface.py
new file mode 100644
index 00000000..d0c5dcb6
--- /dev/null
+++ b/supvisors/tests/test_eventinterface.py
@@ -0,0 +1,373 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# ======================================================================
+# Copyright 2022 Julien LE CLEACH
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ======================================================================
+
+import time
+from unittest.mock import call, Mock
+
+import pytest
+
+from supvisors.eventinterface import *
+
+
+@pytest.fixture
+def zmq_import():
+    return pytest.importorskip('zmq')
+
+
+@pytest.fixture
+def zmq_fail_import(mocker):
+    """ Mock ImportError on optional pyzmq if installed to force testing. """
+    mocker.patch.dict('sys.modules', {'zmq': None})
+
+
+@pytest.fixture
+def ws_import():
+    return pytest.importorskip('websockets')
+
+
+@pytest.fixture
+def ws_fail_import(mocker):
+    """ Mock ImportError on optional websockets if installed to force testing. """
+    mocker.patch.dict('sys.modules', {'websockets': None})
+
+
+def test_interface():
+    """ Test the EventPublisherInterface abstract class. """
+    intf = EventPublisherInterface()
+    with pytest.raises(NotImplementedError):
+        intf.close()
+    with pytest.raises(NotImplementedError):
+        intf.send_supvisors_status({})
+    with pytest.raises(NotImplementedError):
+        intf.send_instance_status({})
+    with pytest.raises(NotImplementedError):
+        intf.send_application_status({})
+    with pytest.raises(NotImplementedError):
+        intf.send_process_event('10.0.0.1', {})
+    with pytest.raises(NotImplementedError):
+        intf.send_process_status({})
+    with pytest.raises(NotImplementedError):
+        intf.send_host_statistics({})
+    with pytest.raises(NotImplementedError):
+        intf.send_process_statistics({})
+
+
+def test_create_external_publisher_none(supvisors):
+    """ Test the create_external_publisher function with no event link selected. """
+    assert create_external_publisher(supvisors) is None
+
+
+def test_create_external_publisher_zmq_fail(zmq_import, zmq_fail_import, supvisors):
+    """ Test the create_external_publisher function with zmq event link but not installed. """
+    supvisors.options.event_link = EventLinks.ZMQ
+    assert create_external_publisher(supvisors) is None
+
+
+def test_create_external_publisher_zmq(zmq_import, supvisors):
+    """ Test the make_supvisors_rpcinterface function with zmq event link. """
+    from supvisors.supvisorszmq import ZmqEventPublisher
+    supvisors.options.event_link = EventLinks.ZMQ
+    instance = create_external_publisher(supvisors)
+    assert isinstance(instance, ZmqEventPublisher)
+
+
+def test_create_external_publisher_ws_fail(ws_import, ws_fail_import, supvisors):
+    """ Test the create_external_publisher function with websocket event link but not installed. """
+    supvisors.options.event_link = EventLinks.WS
+    assert create_external_publisher(supvisors) is None
+
+
+def test_create_external_publisher_ws(ws_import, supvisors):
+    """ Test the make_supvisors_rpcinterface function with websocket event link. """
+    from supvisors.supvisorswebsocket import WsEventPublisher
+    supvisors.options.event_link = EventLinks.WS
+    instance = create_external_publisher(supvisors)
+    try:
+        assert isinstance(instance, WsEventPublisher)
+    finally:
+        time.sleep(0.5)
+        instance.close()
+
+
+def test_event_subscriber_interface():
+    """ Test the EventSubscriberInterface class. """
+    interface = EventSubscriberInterface()
+    with pytest.raises(NotImplementedError):
+        interface.on_supvisors_status('dummy body')
+    with pytest.raises(NotImplementedError):
+        interface.on_instance_status('dummy body')
+    with pytest.raises(NotImplementedError):
+        interface.on_application_status('dummy body')
+    with pytest.raises(NotImplementedError):
+        interface.on_process_event('dummy body')
+    with pytest.raises(NotImplementedError):
+        interface.on_process_status('dummy body')
+    with pytest.raises(NotImplementedError):
+        interface.on_host_statistics('dummy body')
+    with pytest.raises(NotImplementedError):
+        interface.on_process_statistics('dummy body')
+
+
+def test_event_subscriber_creation(supvisors):
+    """ Test the EventSubscriber constructor. """
+    interface = EventSubscriberInterface()
+    subscriber = EventSubscriber(interface, 'localhost', 7777, supvisors.logger)
+    assert subscriber.intf is interface
+    assert subscriber.node_name == 'localhost'
+    assert subscriber.event_port == 7777
+    assert subscriber.logger is supvisors.logger
+    assert subscriber.headers == set()
+    assert subscriber.thread is not None
+    assert isinstance(subscriber.thread, AsyncEventThread)
+    assert not subscriber.thread.is_alive()
+
+
+@pytest.fixture
+def interface():
+    return Mock(spec=EventSubscriberInterface)
+
+
+@pytest.fixture
+def subscriber(supvisors, interface):
+    return EventSubscriber(interface, 'localhost', 7777, supvisors.logger)
+
+
+def test_event_subscriber_mainloop(subscriber):
+    """ Test the EventSubscriber mainloop abstract method. """
+    coro = subscriber.mainloop(asyncio.Event(), 'localhost', 7777)
+    with pytest.raises(NotImplementedError):
+        asyncio.get_event_loop().run_until_complete(coro)
+
+
+async def mocked_loop(*args):
+    counter = 5
+    while counter > 0 and not args[0].is_set():
+        await asyncio.sleep(0.5)
+        counter -= 1
+
+
+def test_event_subscriber_thread(mocker, subscriber):
+    """ Test the EventSubscriber class. """
+    # patch the mainloop to avoid that the thread exits immediately
+    mocker.patch.object(subscriber, 'mainloop', side_effect=mocked_loop)
+    # reset so that the coroutine is taken into account by the async thread
+    subscriber.reset()
+    # test that calling stop before start is not an issue
+    subscriber.stop()
+    assert not subscriber.thread.is_alive()
+    # start the thread
+    subscriber.start()
+    time.sleep(1)
+    assert subscriber.thread.is_alive()
+    # stop the thread
+    subscriber.stop()
+    assert not subscriber.thread.is_alive()
+
+
+def test_event_subscriber_on_receive_supvisors_status(interface, subscriber):
+    """ Test the reception of a Supvisors status message in the EventSubscriber class. """
+    subscriber.on_receive(EventHeaders.SUPVISORS.value, 'supvisors status')
+    assert interface.on_supvisors_status.call_args_list == [call('supvisors status')]
+    assert not interface.on_instance_status.called
+    assert not interface.on_application_status.called
+    assert not interface.on_process_event.called
+    assert not interface.on_process_status.called
+    assert not interface.on_host_statistics.called
+    assert not interface.on_process_statistics.called
+
+
+def test_event_subscriber_on_receive_instance_status(interface, subscriber):
+    """ Test the reception of an Instance status message in the EventSubscriber class. """
+    subscriber.on_receive(EventHeaders.INSTANCE.value, 'instance status')
+    assert interface.on_instance_status.call_args_list == [call('instance status')]
+    assert not interface.on_supvisors_status.called
+    assert not interface.on_application_status.called
+    assert not interface.on_process_event.called
+    assert not interface.on_process_status.called
+    assert not interface.on_host_statistics.called
+    assert not interface.on_process_statistics.called
+
+
+def test_event_subscriber_on_receive_application_status(interface, subscriber):
+    """ Test the reception of an Application status message in the EventSubscriber class. """
+    subscriber.on_receive(EventHeaders.APPLICATION.value, 'application status')
+    assert interface.on_application_status.call_args_list == [call('application status')]
+    assert not interface.on_supvisors_status.called
+    assert not interface.on_instance_status.called
+    assert not interface.on_process_event.called
+    assert not interface.on_process_status.called
+    assert not interface.on_host_statistics.called
+    assert not interface.on_process_statistics.called
+
+
+def test_event_subscriber_on_receive_process_event(interface, subscriber):
+    """ Test the reception of a Supvisors status message in the EventSubscriber class. """
+    subscriber.on_receive(EventHeaders.PROCESS_EVENT.value, 'process event')
+    assert interface.on_process_event.call_args_list == [call('process event')]
+    assert not interface.on_supvisors_status.called
+    assert not interface.on_instance_status.called
+    assert not interface.on_application_status.called
+    assert not interface.on_process_status.called
+    assert not interface.on_host_statistics.called
+    assert not interface.on_process_statistics.called
+
+
+def test_event_subscriber_on_receive_process_status(interface, subscriber):
+    """ Test the reception of a Process status message in the EventSubscriber class. """
+    subscriber.on_receive(EventHeaders.PROCESS_STATUS.value, 'process status')
+    assert interface.on_process_status.call_args_list == [call('process status')]
+    assert not interface.on_supvisors_status.called
+    assert not interface.on_instance_status.called
+    assert not interface.on_application_status.called
+    assert not interface.on_process_event.called
+    assert not interface.on_host_statistics.called
+    assert not interface.on_process_statistics.called
+
+
+def test_event_subscriber_on_receive_host_statistics(interface, subscriber):
+    """ Test the reception of a Host Statistics message in the EventSubscriber class. """
+    subscriber.on_receive(EventHeaders.HOST_STATISTICS.value, 'host statistics')
+    assert interface.on_host_statistics.call_args_list == [call('host statistics')]
+    assert not interface.on_supvisors_status.called
+    assert not interface.on_instance_status.called
+    assert not interface.on_application_status.called
+    assert not interface.on_process_event.called
+    assert not interface.on_process_status.called
+    assert not interface.on_process_statistics.called
+
+
+def test_event_subscriber_on_receive_process_statistics(interface, subscriber):
+    """ Test the reception of a Process Statistics message in the EventSubscriber class. """
+    subscriber.on_receive(EventHeaders.PROCESS_STATISTICS.value, 'process statistics')
+    assert interface.on_process_statistics.call_args_list == [call('process statistics')]
+    assert not interface.on_supvisors_status.called
+    assert not interface.on_instance_status.called
+    assert not interface.on_application_status.called
+    assert not interface.on_process_event.called
+    assert not interface.on_process_status.called
+    assert not interface.on_host_statistics.called
+
+
+def test_event_subscriber_on_receive_unknown(interface, subscriber):
+    """ Test the reception of an unknown message in the EventSubscriber class. """
+    with pytest.raises(ValueError):
+        subscriber.on_receive('dummy header', 'dummy body')
+    assert not interface.on_supvisors_status.called
+    assert not interface.on_instance_status.called
+    assert not interface.on_application_status.called
+    assert not interface.on_process_event.called
+    assert not interface.on_process_status.called
+    assert not interface.on_host_statistics.called
+    assert not interface.on_process_statistics.called
+
+
+def test_event_subscriber_subscriptions(subscriber):
+    """ Test the EventSubscriber subscription and unsubscription methods. """
+    assert subscriber.headers == set()
+    subscriber.subscribe_all()
+    all_headers = {'application', 'hstats', 'instance', 'event', 'pstats', 'process', 'supvisors'}
+    assert subscriber.headers == all_headers
+    assert subscriber.all_subscriptions()
+    # test that subscribe to anything already subscribed works and does not change anything
+    for header in EventHeaders:
+        subscriber.subscribe(header)
+        assert subscriber.headers == all_headers
+        assert subscriber.all_subscriptions()
+    # unsubscribe one by one
+    subscriber.unsubscribe_supvisors_status()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'application', 'hstats', 'instance', 'event', 'pstats', 'process'}
+    subscriber.unsubscribe_instance_status()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'application', 'hstats', 'event', 'pstats', 'process'}
+    subscriber.unsubscribe_application_status()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'hstats', 'event', 'pstats', 'process'}
+    subscriber.unsubscribe_process_event()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'hstats', 'pstats', 'process'}
+    subscriber.unsubscribe_process_status()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'hstats', 'pstats'}
+    subscriber.unsubscribe_host_statistics()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'pstats'}
+    subscriber.unsubscribe_process_statistics()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == set()
+    # test that unsubscribe from anything already unsubscribed works and does not change anything
+    for header in EventHeaders:
+        subscriber.unsubscribe(header)
+        assert subscriber.headers == set()
+        assert not subscriber.all_subscriptions()
+    # subscribe one by one
+    subscriber.subscribe_process_statistics()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'pstats'}
+    subscriber.subscribe_host_statistics()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'hstats', 'pstats'}
+    subscriber.subscribe_process_status()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'hstats', 'pstats', 'process'}
+    subscriber.subscribe_process_event()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'hstats', 'event', 'pstats', 'process'}
+    subscriber.subscribe_application_status()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'application', 'hstats', 'event', 'pstats', 'process'}
+    subscriber.subscribe_instance_status()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == {'application', 'hstats', 'instance', 'event', 'pstats', 'process'}
+    subscriber.subscribe_supvisors_status()
+    assert subscriber.all_subscriptions()
+    assert subscriber.headers == all_headers
+    # unsubscribe from all
+    subscriber.unsubscribe_all()
+    assert not subscriber.all_subscriptions()
+    assert subscriber.headers == set()
+
+
+@pytest.fixture
+def event_thread():
+    return AsyncEventThread(mocked_loop, 'localhost', 7777)
+
+
+def test_async_event_thread_creation(event_thread):
+    """ Test the EventSubscriber subscription and unsubscription methods. """
+    assert event_thread.coro is mocked_loop
+    assert event_thread.node_name == 'localhost'
+    assert event_thread.event_port == 7777
+    assert event_thread.loop is None
+    assert event_thread.stop_event is None
+
+
+def test_async_event_thread_run_stop(event_thread):
+    """ Test the EventSubscriber class. """
+    # test that calling stop before start is not an issue
+    event_thread.stop()
+    assert not event_thread.is_alive()
+    # start the thread
+    event_thread.start()
+    time.sleep(1)
+    assert event_thread.is_alive()
+    # stop the thread
+    event_thread.stop()
+    event_thread.join()
+    assert not event_thread.is_alive()
diff --git a/supvisors/tests/test_flask.py b/supvisors/tests/test_flask.py
index 6db9dfe2..47b3ce25 100644
--- a/supvisors/tests/test_flask.py
+++ b/supvisors/tests/test_flask.py
@@ -1,6 +1,5 @@
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
-
 # ======================================================================
 # Copyright 2022 Julien LE CLEACH
 #
@@ -654,13 +653,20 @@ def test_supvisors_start_args(xml_rpc, client):
     mocked_func = xml_rpc.supvisors.start_args
     # test error with missing parameter
     check_post_error(client, f'{base_url}', mocked_func)
-    check_post_error(client, f'{base_url}/my_movies:converter_02', mocked_func)
     # test with parameters
-    check_post_success(client, f'{base_url}/my_movies:converter_02/-x 2', mocked_func,
+    check_post_success(client, f'{base_url}/my_movies:converter_02', mocked_func,
+                       [call('my_movies:converter_02', '', True)])
+    mocked_func.reset_mock()
+    check_post_success(client, f'{base_url}/my_movies:converter_02?extra_args=-x 2', mocked_func,
                        [call('my_movies:converter_02', '-x 2', True)])
     mocked_func.reset_mock()
-    check_post_success(client, f'{base_url}/my_movies:converter_02/-x%202?wait=false', mocked_func,
+    check_post_success(client, f'{base_url}/my_movies:converter_02?extra_args=-x%202&wait=false', mocked_func,
                        [call('my_movies:converter_02', '-x 2', False)])
+    mocked_func.reset_mock()
+    # test with path parameters
+    file_path = os.path.abspath(__file__)
+    check_post_success(client, f'{base_url}/my_movies:converter_02?extra_args={file_path}&wait=false', mocked_func,
+                       [call('my_movies:converter_02', file_path, False)])
 
 
 def test_supvisors_start_process(xml_rpc, client):
diff --git a/supvisors/tests/test_initializer.py b/supvisors/tests/test_initializer.py
index c47c129f..b8e7aa36 100644
--- a/supvisors/tests/test_initializer.py
+++ b/supvisors/tests/test_initializer.py
@@ -22,6 +22,8 @@
 import pytest
 
 from supvisors.initializer import *
+from supvisors.statscollector import ProcessStatisticsCollector, instant_host_statistics
+from supvisors.statscompiler import HostStatisticsCompiler, ProcStatisticsCompiler
 from .base import DummySupervisor
 
 
@@ -43,10 +45,13 @@ def test_creation(mocker):
     assert isinstance(supv.logger, Logger)
     assert isinstance(supv.supervisor_data, SupervisorData)
     assert isinstance(supv.supvisors_mapper, SupvisorsMapper)
+    assert supv.host_collector is instant_host_statistics
+    assert isinstance(supv.process_collector, ProcessStatisticsCollector)
     assert isinstance(supv.context, Context)
     assert isinstance(supv.starter, Starter)
     assert isinstance(supv.stopper, Stopper)
-    assert isinstance(supv.statistician, StatisticsCompiler)
+    assert isinstance(supv.host_compiler, HostStatisticsCompiler)
+    assert isinstance(supv.process_compiler, ProcStatisticsCompiler)
     assert isinstance(supv.fsm, FiniteStateMachine)
     assert supv.parser == 'Parser'
     assert isinstance(supv.listener, SupervisorListener)
@@ -77,6 +82,17 @@ def test_identifier_exception(mocker):
         Supvisors(supervisord_instance)
 
 
+def test_psutil_exception(mocker):
+    """ Test the values set at construction. """
+    mocker.patch('supvisors.initializer.SupvisorsServerOptions')
+    mocker.patch.dict('sys.modules', {'supvisors.statscollector': None})
+    # create Supvisors instance
+    supvisors = Supvisors(DummySupervisor())
+    # test that parser exception is accepted
+    assert supvisors.host_collector is None
+    assert supvisors.process_collector is None
+
+
 def test_parser_exception(mocker):
     """ Test the values set at construction. """
     mocker.patch('supvisors.initializer.Parser', side_effect=Exception)
diff --git a/supvisors/tests/test_instancestatus.py b/supvisors/tests/test_instancestatus.py
index 71b3adae..e872b6d0 100644
--- a/supvisors/tests/test_instancestatus.py
+++ b/supvisors/tests/test_instancestatus.py
@@ -18,17 +18,16 @@
 # ======================================================================
 
 import pickle
-import pytest
 import random
 import time
 
+import pytest
 from supervisor.states import ProcessStates
 
 from supvisors.instancestatus import *
 from supvisors.ttypes import SupvisorsInstanceStates, InvalidTransition
-
-from .base import database_copy
-from .conftest import create_any_process, create_process
+from .base import database_copy, any_process_info_by_state
+from .conftest import create_process
 
 
 def test_state_modes():
@@ -69,23 +68,35 @@ def supvisors_id(supvisors):
     return SupvisorsInstanceId('<supvisors>10.0.0.1:65000:65001', supvisors)
 
 
+@pytest.fixture
+def local_supvisors_id(supvisors):
+    """ Create a SupvisorsInstanceId. """
+    return SupvisorsInstanceId(f'<{supvisors.supvisors_mapper.local_identifier}>10.0.0.1:65000:65001', supvisors)
+
+
 @pytest.fixture
 def status(supvisors, supvisors_id):
     """ Create an empty SupvisorsInstanceStatus. """
     return SupvisorsInstanceStatus(supvisors_id, supvisors)
 
 
+@pytest.fixture
+def local_status(supvisors, local_supvisors_id):
+    """ Create an empty SupvisorsInstanceStatus. """
+    return SupvisorsInstanceStatus(local_supvisors_id, supvisors)
+
+
 @pytest.fixture
 def filled_status(supvisors, status):
     """ Create a SupvisorsInstanceStatus and add all processes of the database. """
     for info in database_copy():
         process = create_process(info, supvisors)
-        process.add_info('supvisors', info)
+        process.add_info(status.supvisors_id.identifier, info)
         status.add_process(process)
     return status
 
 
-def test_create(supvisors, supvisors_id, status):
+def test_create_no_collector(supvisors, supvisors_id, status):
     """ Test the values set at SupvisorsInstanceStatus construction. """
     assert status.supvisors is supvisors
     assert status.logger is supvisors.logger
@@ -99,6 +110,28 @@ def test_create(supvisors, supvisors_id, status):
     assert status.local_time == 0.0
     assert status.processes == {}
     assert status.state_modes == StateModes()
+    # process_collector is None because local_identifier is different in supvisors_mapper and in SupvisorsInstanceId
+    assert status.process_collector is None
+
+
+def test_create_collector(supvisors, local_supvisors_id, local_status):
+    """ Test the values set at SupvisorsInstanceStatus construction. """
+    assert local_status.supvisors is supvisors
+    assert local_status.logger is supvisors.logger
+    assert local_status.supvisors_id is local_supvisors_id
+    assert local_status.identifier == supvisors.supvisors_mapper.local_identifier
+    assert local_status.state == SupvisorsInstanceStates.UNKNOWN
+    assert local_status.sequence_counter == 0
+    assert local_status.local_sequence_counter == 0
+    assert local_status.start_time == 0.0
+    assert local_status.remote_time == 0.0
+    assert local_status.local_time == 0.0
+    assert local_status.processes == {}
+    assert local_status.state_modes == StateModes()
+    # process_collector is set as SupvisorsInstanceId and supvisors_mapper's local_identifier are identical
+    #  and the option process_stats_enabled is True
+    assert local_status.process_collector is not None
+    assert local_status.process_collector is supvisors.process_collector
 
 
 def test_reset(status):
@@ -129,8 +162,8 @@ def test_serialization(status):
     serialized = status.serial()
     assert serialized == {'identifier': 'supvisors', 'node_name': '10.0.0.1', 'port': 65000, 'loading': 0,
                           'statecode': 2, 'statename': 'RUNNING', 'remote_time': 50, 'local_time': 60,
-                          'sequence_counter': 28, 'fsm_statecode': 0, 'fsm_statename': 'OFF',
-                          'starting_jobs': False, 'stopping_jobs': False}
+                          'sequence_counter': 28, 'process_failure': False,
+                          'fsm_statecode': 0, 'fsm_statename': 'OFF', 'starting_jobs': False, 'stopping_jobs': False}
     # test that returned structure is serializable using pickle
     dumped = pickle.dumps(serialized)
     loaded = pickle.loads(dumped)
@@ -214,13 +247,106 @@ def test_isolation(status):
                                                             SupvisorsInstanceStates.ISOLATED])
 
 
-def test_add_process(supvisors, status):
-    """ Test the SupvisorsInstanceStatus.add_process method. """
-    process = create_any_process(supvisors)
+def test_process_no_collector(supvisors, status):
+    """ Test the SupvisorsInstanceStatus.xxx_process methods when no process collector is available. """
+    info = any_process_info_by_state(ProcessStates.RUNNING)
+    process = create_process(info, supvisors)
     status.add_process(process)
+    status.update_process(process)
+    # check no process_collector
+    assert status.process_collector is None
     # check that process is stored
     assert process.namespec in status.processes.keys()
     assert process is status.processes[process.namespec]
+    # test remove process
+    status.remove_process(process)
+    # check that process is stored anymore
+    assert process.namespec not in status.processes.keys()
+
+
+def test_process_running_unknown_collector(supvisors, local_status):
+    """ Test the SupvisorsInstanceStatus.xxx_process methods when no pid running on the identifier. """
+    info = any_process_info_by_state(ProcessStates.RUNNING)
+    process = create_process(info, supvisors)
+    local_status.add_process(process)
+    # check process_collector
+    assert local_status.process_collector is not None
+    # check that process is stored
+    assert process.namespec in local_status.processes.keys()
+    assert process is local_status.processes[process.namespec]
+    # check data in process_collector queue
+    assert local_status.process_collector.pid_queue.empty()
+    # on process update, send the pid again or 0 if not found
+    local_status.update_process(process)
+    assert not local_status.process_collector.pid_queue.empty()
+    data = local_status.process_collector.pid_queue.get()
+    assert data == (process.namespec, 0)
+    # test remove process
+    local_status.remove_process(process)
+    # check that process is stored anymore
+    assert process.namespec not in local_status.processes.keys()
+    # on process removal, pid 0 is sent to the collector
+    assert not local_status.process_collector.pid_queue.empty()
+    data = local_status.process_collector.pid_queue.get()
+    assert data == (process.namespec, 0)
+
+
+def test_add_process_running_collector(supvisors, local_status):
+    """ Test the SupvisorsInstanceStatus.add_process method when the process is running on the identifier. """
+    info = any_process_info_by_state(ProcessStates.RUNNING)
+    process = create_process(info, supvisors)
+    process.add_info(local_status.identifier, info)
+    local_status.add_process(process)
+    # check process_collector
+    assert local_status.process_collector is not None
+    # check that process is stored
+    assert process.namespec in local_status.processes.keys()
+    assert process is local_status.processes[process.namespec]
+    # check data in process_collector queue
+    assert not local_status.process_collector.pid_queue.empty()
+    data = local_status.process_collector.pid_queue.get()
+    assert data == (process.namespec, info['pid'])
+    # on process update, send the pid again
+    local_status.update_process(process)
+    assert not local_status.process_collector.pid_queue.empty()
+    data = local_status.process_collector.pid_queue.get()
+    assert data == (process.namespec, info['pid'])
+    # test remove process
+    local_status.remove_process(process)
+    # check that process is stored anymore
+    assert process.namespec not in local_status.processes.keys()
+    # on process removal, pid 0 is sent to the collector
+    assert not local_status.process_collector.pid_queue.empty()
+    data = local_status.process_collector.pid_queue.get()
+    assert data == (process.namespec, 0)
+
+
+def test_add_process_stopped_collector(supvisors, local_status):
+    """ Test the SupvisorsInstanceStatus.add_process method when the process is stopped on the identifier. """
+    info = any_process_info_by_state(ProcessStates.STOPPED)
+    process = create_process(info, supvisors)
+    process.add_info(local_status.identifier, info)
+    local_status.add_process(process)
+    # check process_collector
+    assert local_status.process_collector is not None
+    # check that process is stored
+    assert process.namespec in local_status.processes.keys()
+    assert process is local_status.processes[process.namespec]
+    # check data in process_collector queue
+    assert local_status.process_collector.pid_queue.empty()
+    # on process update, send the pid again
+    local_status.update_process(process)
+    assert not local_status.process_collector.pid_queue.empty()
+    data = local_status.process_collector.pid_queue.get()
+    assert data == (process.namespec, 0)
+    # test remove process
+    local_status.remove_process(process)
+    # check that process is stored anymore
+    assert process.namespec not in local_status.processes.keys()
+    # on process removal, pid 0 is sent to the collector
+    assert not local_status.process_collector.pid_queue.empty()
+    data = local_status.process_collector.pid_queue.get()
+    assert data == (process.namespec, 0)
 
 
 def test_update_times(filled_status):
@@ -274,12 +400,6 @@ def test_running_process(filled_status):
     assert {proc.process_name for proc in filled_status.running_processes()} == expected
 
 
-def test_pid_process(filled_status):
-    """ Test the SupvisorsInstanceStatus.pid_process method. """
-    # check the namespec and pid of the running processes
-    assert {('sample_test_1:xfontsel', 80879), ('sample_test_2:yeux_01', 80882)} == set(filled_status.pid_processes())
-
-
 def test_get_load(filled_status):
     """ Test the SupvisorsInstanceStatus.get_load method. """
     # check the loading of the address: gives 0 by default because no rule has been loaded
@@ -292,3 +412,36 @@ def test_get_load(filled_status):
     process = random.choice([proc for proc in filled_status.processes.values() if proc.running()])
     process.rules.expected_load = 50
     assert filled_status.get_load() == 50
+
+
+def test_has_no_error(status):
+    """ Test the SupvisorsInstanceStatus.has_error method. """
+    # test that has_error is not raised when there is no error
+    for state in SupvisorsInstanceStates:
+        status._state = state
+        assert not status.has_error()
+
+
+def test_has_error(filled_status):
+    """ Test the SupvisorsInstanceStatus.has_error method. """
+    # test that has_error is raised only in RUNNING state when there is an error
+    for state in SupvisorsInstanceStates:
+        filled_status._state = state
+        assert not filled_status.has_error() or state == SupvisorsInstanceStates.RUNNING
+    # replace any FATAL process by unexpected EXITED
+    filled_status._state = SupvisorsInstanceStates.RUNNING
+    for process in filled_status.processes.values():
+        info = process.info_map[filled_status.supvisors_id.identifier]
+        if info['state'] == ProcessStates.FATAL:
+            info['state'] = ProcessStates.EXITED
+            info['expected'] = False
+    # error still raised
+    assert filled_status.has_error()
+    # replace any EXITED process by STOPPED
+    filled_status._state = SupvisorsInstanceStates.RUNNING
+    for process in filled_status.processes.values():
+        info = process.info_map[filled_status.supvisors_id.identifier]
+        if info['state'] == ProcessStates.EXITED:
+            info['state'] = ProcessStates.STOPPED
+    # error still raised
+    assert not filled_status.has_error()
diff --git a/supvisors/tests/test_listener.py b/supvisors/tests/test_listener.py
index 9a20dba0..056b9dcc 100644
--- a/supvisors/tests/test_listener.py
+++ b/supvisors/tests/test_listener.py
@@ -23,8 +23,8 @@
 from supervisor.events import *
 from supervisor.xmlrpc import Faults
 
+from supvisors.eventinterface import EventPublisherInterface
 from supvisors.listener import *
-from supvisors.publisherinterface import EventPublisherInterface
 
 
 @pytest.fixture
@@ -39,10 +39,10 @@ def test_creation_no_collector(mocker, supvisors):
     listener = SupervisorListener(supvisors)
     # check attributes
     assert listener.supvisors == supvisors
-    assert listener.collector is None
     assert listener.local_identifier == supvisors.supvisors_mapper.local_identifier
     assert listener.main_loop is None
     assert listener.publisher is None
+    assert listener.external_publisher is None
     # test that callbacks are set in Supervisor
     assert (SupervisorRunningEvent, listener.on_running) in callbacks
     assert (SupervisorStoppingEvent, listener.on_stopping) in callbacks
@@ -57,15 +57,14 @@ def test_creation_no_collector(mocker, supvisors):
     assert (RemoteCommunicationEvent, listener.on_remote_event) in callbacks
 
 
-def test_creation(mocker, supvisors, listener):
+def test_creation(supvisors, listener):
     """ Test the values set at construction. """
-    mocked_collector = mocker.patch.object(listener, 'collector')
     # check attributes
     assert listener.supvisors is supvisors
-    assert listener.collector is mocked_collector
     assert listener.local_identifier == supvisors.supvisors_mapper.local_identifier
     assert listener.main_loop is None
     assert listener.publisher is None
+    assert listener.external_publisher is None
     # test that callbacks are set in Supervisor
     assert (SupervisorRunningEvent, listener.on_running) in callbacks
     assert (SupervisorStoppingEvent, listener.on_stopping) in callbacks
@@ -92,17 +91,23 @@ def test_on_running(mocker, listener):
     ref_main_loop = listener.main_loop
     mocked_prepare = mocker.patch.object(listener.supvisors.supervisor_data, 'update_supervisor')
     mocked_sockets = mocker.patch('supvisors.listener.SupvisorsSockets')
-    mocked_publisher = mocker.patch('supvisors.listener.create_external_publisher')
+    mocked_external_publisher = Mock()
+    mocked_publisher_creation = mocker.patch('supvisors.listener.create_external_publisher',
+                                             return_value=mocked_external_publisher)
     mocked_loop = mocker.patch('supvisors.listener.SupvisorsMainLoop')
+    mocked_collect = mocker.patch.object(listener.supvisors.process_collector, 'start')
     listener.on_running('')
     # test attributes and calls
     assert mocked_prepare.called
     assert mocked_sockets.called
-    assert mocked_publisher.called
+    assert mocked_publisher_creation.called
     assert listener.publisher is not ref_publisher
+    assert listener.external_publisher is mocked_external_publisher
+    assert listener.supvisors.external_publisher is listener.external_publisher
     assert mocked_loop.called
     assert listener.main_loop is not ref_main_loop
     assert listener.main_loop.start.called
+    assert mocked_collect.called
 
 
 def test_on_stopping_exception(mocker, listener):
@@ -118,20 +123,21 @@ def test_on_stopping(mocker, listener):
     listener.main_loop = Mock(**{'stop.return_value': None})
     mocked_infosource = mocker.patch.object(listener.supvisors.supervisor_data, 'close_httpservers')
     # create an external publisher patch
-    listener.supvisors.external_publisher = Mock(spec=EventPublisherInterface)
+    listener.external_publisher = Mock(spec=EventPublisherInterface)
     # 1. test with unmarked logger, i.e. meant to be the supervisor logger
     listener.on_stopping('')
     assert callbacks == []
     assert mocked_infosource.called
     assert listener.main_loop.stop.called
     assert listener.supvisors.sockets.stop.called
-    assert listener.supvisors.external_publisher.close.called
+    assert listener.external_publisher.close.called
     assert not listener.supvisors.logger.close.called
+    assert listener.supvisors.process_collector.pid_queue.get() is None
     # reset mocks
     mocked_infosource.reset_mock()
     listener.main_loop.stop.reset_mock()
     listener.supvisors.sockets.stop.reset_mock()
-    listener.supvisors.external_publisher.close.reset_mock()
+    listener.external_publisher.close.reset_mock()
     # 2. test with marked logger, i.e. meant to be the Supvisors logger
     listener.logger.SUPVISORS = None
     listener.on_stopping('')
@@ -139,8 +145,9 @@ def test_on_stopping(mocker, listener):
     assert mocked_infosource.called
     assert listener.main_loop.stop.called
     assert listener.supvisors.sockets.stop.called
-    assert listener.supvisors.external_publisher.close.called
+    assert listener.external_publisher.close.called
     assert listener.supvisors.logger.close.called
+    assert listener.supvisors.process_collector.pid_queue.get() is None
 
 
 def test_on_process_state_exception(listener):
@@ -304,35 +311,51 @@ def test_on_tick_exception(listener):
 def test_on_tick(mocker, listener):
     """ Test the reception of a Supervisor TICK event. """
     mocker.patch('time.time', return_value=1234.56)
-    mocker.patch.object(listener, 'collector', return_value=(8.5, [(25, 400)], 76.1, {'lo': (500, 500)}, {}))
+    mocker.patch.object(listener.supvisors, 'host_collector', return_value=(8.5, [(25, 400)], 76.1, {'lo': (500, 500)}))
     # create patches
     listener.publisher = Mock(**{'send_tick_event.return_value': None,
                                  'send_statistics.return_value': None})
     listener.supvisors.context.instances['127.0.0.1'] = Mock(**{'pid_processes.return_value': []})
+    # add some data to the process collector
+    listener.supvisors.process_collector.stats_queue.put({'namespec': 'dummy_1'})
+    listener.supvisors.process_collector.stats_queue.put({'namespec': 'dummy_2'})
     # test process event
     event = Tick60Event(120, None)
     listener.on_tick(event)
     assert listener.publisher.send_tick_event.call_args_list == [call({'when': 1234.56, 'sequence_counter': 0})]
-    assert listener.publisher.send_statistics.call_args_list == [call((8.5, [(25, 400)], 76.1, {'lo': (500, 500)}, {}))]
+    assert listener.publisher.send_host_statistics.call_args_list == [call((8.5, [(25, 400)], 76.1,
+                                                                            {'lo': (500, 500)}))]
+    assert listener.publisher.send_process_statistics.call_args_list == [call([{'namespec': 'dummy_1'},
+                                                                               {'namespec': 'dummy_2'}])]
     listener.publisher.reset_mock()
-    # test process event when statistics disabled
+    # test process event when host collector is not available
+    ref_host_collector = listener.supvisors.host_collector
+    listener.supvisors.host_collector = None
     event = Tick60Event(150, None)
-    listener.supvisors.options.stats_enabled = False
     listener.on_tick(event)
     assert listener.publisher.send_tick_event.call_args_list == [call({'when': 1234.56, 'sequence_counter': 1})]
-    assert not listener.publisher.send_statistics.called
+    assert not listener.publisher.send_host_statistics.called
+    assert not listener.publisher.send_process_statistics.called
     listener.publisher.reset_mock()
-    # test process event when statistics collector is not available
+    listener.supvisors.host_collector = ref_host_collector
+    # add some data to the process collector
+    listener.supvisors.process_collector.stats_queue.put({'namespec': 'dummy_1'})
+    listener.supvisors.process_collector.stats_queue.put({'namespec': 'dummy_2'})
+    # test process event when process collector is not available
     event = Tick60Event(150, None)
-    listener.supvisors.options.stats_enabled = True
+    listener.supvisors.process_collector = None
     listener.collector = None
     listener.on_tick(event)
     assert listener.publisher.send_tick_event.call_args_list == [call({'when': 1234.56, 'sequence_counter': 2})]
-    assert not listener.publisher.send_statistics.called
+    assert listener.publisher.send_host_statistics.call_args_list == [call((8.5, [(25, 400)], 76.1,
+                                                                            {'lo': (500, 500)}))]
+    assert not listener.publisher.send_process_statistics.called
 
 
-def test_unstack_event_heartbeat(listener):
+def test_unstack_event_heartbeat(mocker, listener):
     """ Test the processing of a Supvisors HEARTBEAT event. """
+    mocked_host = mocker.patch.object(listener.supvisors.host_compiler, 'push_statistics')
+    mocked_proc = mocker.patch.object(listener.supvisors.process_compiler, 'push_statistics')
     listener.unstack_event('[0, ["10.0.0.1", []]]')
     assert not listener.supvisors.fsm.on_tick_event.called
     assert not listener.supvisors.fsm.on_process_state_event.called
@@ -340,11 +363,14 @@ def test_unstack_event_heartbeat(listener):
     assert not listener.supvisors.fsm.on_process_removed_event.called
     assert not listener.supvisors.fsm.on_process_disability_event.called
     assert not listener.supvisors.fsm.on_state_event.called
-    assert not listener.supvisors.statistician.push_statistics.called
+    assert not mocked_host.called
+    assert not mocked_proc.called
 
 
-def test_unstack_event_tick(listener):
+def test_unstack_event_tick(mocker, listener):
     """ Test the processing of a Supvisors TICK event. """
+    mocked_host = mocker.patch.object(listener.supvisors.host_compiler, 'push_statistics')
+    mocked_proc = mocker.patch.object(listener.supvisors.process_compiler, 'push_statistics')
     listener.unstack_event('[1, ["10.0.0.1", "data"]]')
     expected = [call('10.0.0.1', 'data')]
     assert listener.supvisors.fsm.on_tick_event.call_args_list == expected
@@ -353,11 +379,14 @@ def test_unstack_event_tick(listener):
     assert not listener.supvisors.fsm.on_process_removed_event.called
     assert not listener.supvisors.fsm.on_process_disability_event.called
     assert not listener.supvisors.fsm.on_state_event.called
-    assert not listener.supvisors.statistician.push_statistics.called
+    assert not mocked_host.called
+    assert not mocked_proc.called
 
 
-def test_unstack_event_process_state(listener):
+def test_unstack_event_process_state(mocker, listener):
     """ Test the processing of a Supvisors process state event. """
+    mocked_host = mocker.patch.object(listener.supvisors.host_compiler, 'push_statistics')
+    mocked_proc = mocker.patch.object(listener.supvisors.process_compiler, 'push_statistics')
     listener.unstack_event('[2, ["10.0.0.2", {"name": "dummy"}]]')
     expected = [call('10.0.0.2', {'name': 'dummy'})]
     assert not listener.supvisors.fsm.on_tick_event.called
@@ -366,11 +395,14 @@ def test_unstack_event_process_state(listener):
     assert not listener.supvisors.fsm.on_process_removed_event.called
     assert not listener.supvisors.fsm.on_process_disability_event.called
     assert not listener.supvisors.fsm.on_state_event.called
-    assert not listener.supvisors.statistician.push_statistics.called
+    assert not mocked_host.called
+    assert not mocked_proc.called
 
 
-def test_unstack_event_process_added(listener):
+def test_unstack_event_process_added(mocker, listener):
     """ Test the processing of a Supvisors process added event. """
+    mocked_host = mocker.patch.object(listener.supvisors.host_compiler, 'push_statistics')
+    mocked_proc = mocker.patch.object(listener.supvisors.process_compiler, 'push_statistics')
     listener.unstack_event('[3, ["10.0.0.1", {"group": "dummy_group", "name": "dummy_process"}]]')
     expected = [call('10.0.0.1', {'group': 'dummy_group', 'name': 'dummy_process'})]
     assert not listener.supvisors.fsm.on_tick_event.called
@@ -379,11 +411,14 @@ def test_unstack_event_process_added(listener):
     assert not listener.supvisors.fsm.on_process_removed_event.called
     assert not listener.supvisors.fsm.on_process_disability_event.called
     assert not listener.supvisors.fsm.on_state_event.called
-    assert not listener.supvisors.statistician.push_statistics.called
+    assert not mocked_host.called
+    assert not mocked_proc.called
 
 
-def test_unstack_event_process_removed(listener):
+def test_unstack_event_process_removed(mocker, listener):
     """ Test the processing of a Supvisors process removed event. """
+    mocked_host = mocker.patch.object(listener.supvisors.host_compiler, 'push_statistics')
+    mocked_proc = mocker.patch.object(listener.supvisors.process_compiler, 'push_statistics')
     listener.unstack_event('[4, ["10.0.0.1", {"group": "dummy_group", "name": "dummy_process"}]]')
     expected = [call('10.0.0.1', {'group': 'dummy_group', 'name': 'dummy_process'})]
     assert not listener.supvisors.fsm.on_tick_event.called
@@ -392,11 +427,14 @@ def test_unstack_event_process_removed(listener):
     assert listener.supvisors.fsm.on_process_removed_event.call_args_list == expected
     assert not listener.supvisors.fsm.on_process_disability_event.called
     assert not listener.supvisors.fsm.on_state_event.called
-    assert not listener.supvisors.statistician.push_statistics.called
+    assert not mocked_host.called
+    assert not mocked_proc.called
 
 
-def test_unstack_event_process_disability(listener):
+def test_unstack_event_process_disability(mocker, listener):
     """ Test the processing of a Supvisors process enabled event. """
+    mocked_host = mocker.patch.object(listener.supvisors.host_compiler, 'push_statistics')
+    mocked_proc = mocker.patch.object(listener.supvisors.process_compiler, 'push_statistics')
     listener.unstack_event('[5, ["10.0.0.1", {"group": "dummy_group", "name": "dummy_process"}]]')
     expected = [call('10.0.0.1', {'group': 'dummy_group', 'name': 'dummy_process'})]
     assert not listener.supvisors.fsm.on_tick_event.called
@@ -405,25 +443,75 @@ def test_unstack_event_process_disability(listener):
     assert not listener.supvisors.fsm.on_process_removed_event.called
     assert listener.supvisors.fsm.on_process_disability_event.call_args_list == expected
     assert not listener.supvisors.fsm.on_state_event.called
-    assert not listener.supvisors.statistician.push_statistics.called
+    assert not mocked_host.called
+    assert not mocked_proc.called
 
 
-def test_unstack_event_statistics(listener):
-    """ Test the processing of a Supvisors statistics event. """
-    listener.unstack_event('[6, ["10.0.0.3", [0, [[20, 30]], {"lo": [100, 200]}, {}]]]')
+def test_unstack_event_host_statistics(mocker, listener):
+    """ Test the processing of a Supvisors host statistics event. """
+    mocked_host = mocker.patch.object(listener.supvisors.host_compiler, 'push_statistics')
+    mocked_proc = mocker.patch.object(listener.supvisors.process_compiler, 'push_statistics', return_value=None)
+    # 1. external_publisher is None
+    listener.unstack_event('[6, ["10.0.0.3", [0, [[20, 30]], {"lo": [100, 200]}]]]')
     assert not listener.supvisors.fsm.on_tick_event.called
     assert not listener.supvisors.fsm.on_process_state_event.called
     assert not listener.supvisors.fsm.on_process_added_event.called
     assert not listener.supvisors.fsm.on_process_removed_event.called
     assert not listener.supvisors.fsm.on_process_disability_event.called
     assert not listener.supvisors.fsm.on_state_event.called
-    expected = [call('10.0.0.3', [0, [[20, 30]], {'lo': [100, 200]}, {}])]
-    assert listener.supvisors.statistician.push_statistics.call_args_list == expected
-
-
-def test_unstack_event_state(listener):
+    assert mocked_host.call_args_list == [call('10.0.0.3', [0, [[20, 30]], {'lo': [100, 200]}])]
+    assert not mocked_proc.called
+    mocked_host.reset_mock()
+    # 2. set external_publisher but still no returned value for push_statistics
+    listener.external_publisher = Mock(**{'send_host_statistics.return_value': None})
+    listener.unstack_event('[6, ["10.0.0.3", [0, [[20, 30]], {"lo": [100, 200]}]]]')
+    assert mocked_host.call_args_list == [call('10.0.0.3', [0, [[20, 30]], {'lo': [100, 200]}])]
+    assert not listener.external_publisher.send_host_statistics.called
+    assert not mocked_proc.called
+    mocked_host.reset_mock()
+    # 3. external_publisher set and integrated value available for push_statistics
+    mocked_host.return_value = [{'uptime': 1234}]
+    listener.unstack_event('[6, ["10.0.0.3", [0, [[20, 30]], {"lo": [100, 200]}]]]')
+    assert mocked_host.call_args_list == [call('10.0.0.3', [0, [[20, 30]], {'lo': [100, 200]}])]
+    assert listener.external_publisher.send_host_statistics.call_args_list == [call({'uptime': 1234})]
+    assert not mocked_proc.called
+
+
+def test_unstack_event_process_statistics(mocker, listener):
+    """ Test the processing of a Supvisors process statistics event. """
+    mocked_host = mocker.patch.object(listener.supvisors.host_compiler, 'push_statistics', return_value=None)
+    mocked_proc = mocker.patch.object(listener.supvisors.process_compiler, 'push_statistics')
+    # 1. external_publisher is None
+    listener.unstack_event('[7, ["10.0.0.3", [{"cpu": [100, 200]}, {"cpu": [50, 20]}]]]')
+    assert not listener.supvisors.fsm.on_tick_event.called
+    assert not listener.supvisors.fsm.on_process_state_event.called
+    assert not listener.supvisors.fsm.on_process_added_event.called
+    assert not listener.supvisors.fsm.on_process_removed_event.called
+    assert not listener.supvisors.fsm.on_process_disability_event.called
+    assert not listener.supvisors.fsm.on_state_event.called
+    assert not mocked_host.called
+    assert mocked_proc.call_args_list == [call('10.0.0.3', [{"cpu": [100, 200]}, {"cpu": [50, 20]}])]
+    mocked_proc.reset_mock()
+    # 2. set external_publisher but still no returned value for push_statistics
+    listener.external_publisher = Mock(**{'send_process_statistics.return_value': None})
+    listener.unstack_event('[7, ["10.0.0.3", [{"cpu": [100, 200]}, {"cpu": [50, 20]}]]]')
+    assert not mocked_host.called
+    assert mocked_proc.call_args_list == [call('10.0.0.3', [{'cpu': [100, 200]}, {'cpu': [50, 20]}])]
+    assert not listener.external_publisher.send_process_statistics.called
+    mocked_proc.reset_mock()
+    # 3. external_publisher set and integrated value available for push_statistics
+    mocked_proc.return_value = [{'uptime': 1234}]
+    listener.unstack_event('[7, ["10.0.0.3", [{"cpu": [100, 200]}, {"cpu": [50, 20]}]]]')
+    assert not mocked_host.called
+    assert mocked_proc.call_args_list == [call('10.0.0.3', [{'cpu': [100, 200]}, {'cpu': [50, 20]}])]
+    assert listener.external_publisher.send_process_statistics.call_args_list == [call({'uptime': 1234})]
+
+
+def test_unstack_event_state(mocker, listener):
     """ Test the processing of a Supvisors state event. """
-    listener.unstack_event('[7, ["10.0.0.1", {"statecode": 10, "statename": "RUNNING"}]]')
+    mocked_host = mocker.patch.object(listener.supvisors.host_compiler, 'push_statistics')
+    mocked_proc = mocker.patch.object(listener.supvisors.process_compiler, 'push_statistics')
+    listener.unstack_event('[8, ["10.0.0.1", {"statecode": 10, "statename": "RUNNING"}]]')
     expected = [call('10.0.0.1', {'statecode': 10, 'statename': 'RUNNING'})]
     assert not listener.supvisors.fsm.on_tick_event.called
     assert not listener.supvisors.fsm.on_process_state_event.called
@@ -431,7 +519,8 @@ def test_unstack_event_state(listener):
     assert not listener.supvisors.fsm.on_process_removed_event.called
     assert not listener.supvisors.fsm.on_process_disability_event.called
     assert listener.supvisors.fsm.on_state_event.call_args_list == expected
-    assert not listener.supvisors.statistician.push_statistics.called
+    assert not mocked_host.called
+    assert not mocked_proc.called
 
 
 def test_unstack_info(listener):
diff --git a/supvisors/tests/test_mainloop.py b/supvisors/tests/test_mainloop.py
index 17452411..efaffd12 100644
--- a/supvisors/tests/test_mainloop.py
+++ b/supvisors/tests/test_mainloop.py
@@ -46,8 +46,7 @@ def test_creation(supvisors, mocked_rpc, main_loop):
     assert isinstance(main_loop, Thread)
     assert main_loop.supvisors is supvisors
     assert not main_loop.stop_event.is_set()
-    server_url = main_loop.supvisors.supervisor_data.supervisord.options.serverurl
-    assert main_loop.srv_url.env == {'SUPERVISOR_SERVER_URL': server_url,
+    assert main_loop.srv_url.env == {'SUPERVISOR_SERVER_URL': f'http://{gethostname()}:65000',
                                      'SUPERVISOR_USERNAME': 'user',
                                      'SUPERVISOR_PASSWORD': 'p@$$w0rd'}
     assert mocked_rpc.call_args_list == [call(main_loop.srv_url.env)]
@@ -412,7 +411,7 @@ def check_call(main_loop, mocked_loop, method_name, request, args):
     # send request
     main_loop.send_request(request, args)
     # test mocked main loop
-    assert main_loop.srv_url.parsed_url.geturl() == 'http://10.0.0.2:65000'
+    assert main_loop.srv_url.env['SUPERVISOR_SERVER_URL'] == 'http://10.0.0.2:65000'
     for key, mocked in mocked_loop.items():
         if key == method_name:
             assert mocked.call_count == 1
diff --git a/supvisors/tests/test_options.py b/supvisors/tests/test_options.py
index 59cc2058..3a360359 100644
--- a/supvisors/tests/test_options.py
+++ b/supvisors/tests/test_options.py
@@ -38,7 +38,8 @@ def config():
             'core_identifiers': 'cliche01,cliche03',
             'disabilities_file': '/tmp/disabilities.json',
             'starting_strategy': 'MOST_LOADED', 'conciliation_strategy': 'SENICIDE',
-            'stats_enabled': 'false', 'stats_periods': '5,60,600', 'stats_histo': '100',
+            'stats_enabled': 'process', 'stats_collecting_period': '2',
+            'stats_periods': '5,50,77.7', 'stats_histo': '100',
             'stats_irix_mode': 'true',
             'tail_limit': '1MB', 'tailf_limit': '512',
             'logfile': '/tmp/supvisors.log', 'logfile_maxbytes': '50KB',
@@ -94,7 +95,9 @@ def test_options_creation(opt):
     assert opt.disabilities_file is None
     assert opt.conciliation_strategy == ConciliationStrategies.USER
     assert opt.starting_strategy == StartingStrategies.CONFIG
-    assert opt.stats_enabled
+    assert opt.host_stats_enabled
+    assert opt.process_stats_enabled
+    assert opt.collecting_period == 5
     assert opt.stats_periods == [10]
     assert opt.stats_histo == 200
     assert not opt.stats_irix_mode
@@ -116,8 +119,10 @@ def test_filled_options_creation(filled_opt):
     assert filled_opt.disabilities_file == '/tmp/disabilities.json'
     assert filled_opt.conciliation_strategy == ConciliationStrategies.SENICIDE
     assert filled_opt.starting_strategy == StartingStrategies.MOST_LOADED
-    assert not filled_opt.stats_enabled
-    assert filled_opt.stats_periods == [5, 60, 600]
+    assert not filled_opt.host_stats_enabled
+    assert filled_opt.process_stats_enabled
+    assert filled_opt.collecting_period == 2.0
+    assert filled_opt.stats_periods == [5, 50, 77.7]
     assert filled_opt.stats_histo == 100
     assert filled_opt.stats_irix_mode
     assert filled_opt.tail_limit == 1024 * 1024
@@ -130,7 +135,8 @@ def test_str(opt):
                         ' event_link=NONE event_port=0'
                         ' auto_fence=False synchro_timeout=15 inactivity_ticks=2 core_identifiers=set()'
                         ' disabilities_file=None conciliation_strategy=USER starting_strategy=CONFIG'
-                        ' stats_enabled=True stats_periods=[10] stats_histo=200'
+                        ' host_stats_enabled=True process_stats_enabled=True'
+                        ' collecting_period=5 stats_periods=[10] stats_histo=200'
                         ' stats_irix_mode=False tail_limit=1024 tailf_limit=1024')
 
 
@@ -163,10 +169,11 @@ def test_port_num():
     # test invalid values
     with pytest.raises(ValueError, match=error_message):
         SupvisorsOptions.to_port_num('-1')
+    with pytest.raises(ValueError, match=error_message):
+        SupvisorsOptions.to_port_num('0')
     with pytest.raises(ValueError, match=error_message):
         SupvisorsOptions.to_port_num('65536')
     # test valid values
-    assert SupvisorsOptions.to_port_num('0') == 0
     assert SupvisorsOptions.to_port_num('1') == 1
     assert SupvisorsOptions.to_port_num('65535') == 65535
 
@@ -227,11 +234,12 @@ def test_conciliation_strategy():
     with pytest.raises(ValueError, match=error_message):
         SupvisorsOptions.to_conciliation_strategy('dummy')
     with pytest.raises(ValueError, match=error_message):
-        SupvisorsOptions.to_conciliation_strategy('user')
+        SupvisorsOptions.to_conciliation_strategy('users')
     # test valid values
     assert SupvisorsOptions.to_conciliation_strategy('SENICIDE') == ConciliationStrategies.SENICIDE
     assert SupvisorsOptions.to_conciliation_strategy('INFANTICIDE') == ConciliationStrategies.INFANTICIDE
     assert SupvisorsOptions.to_conciliation_strategy('USER') == ConciliationStrategies.USER
+    assert SupvisorsOptions.to_conciliation_strategy('user') == ConciliationStrategies.USER
     assert SupvisorsOptions.to_conciliation_strategy('STOP') == ConciliationStrategies.STOP
     assert SupvisorsOptions.to_conciliation_strategy('RESTART') == ConciliationStrategies.RESTART
 
@@ -245,13 +253,55 @@ def test_starting_strategy():
     with pytest.raises(ValueError, match=error_message):
         SupvisorsOptions.to_starting_strategy('dummy')
     with pytest.raises(ValueError, match=error_message):
-        SupvisorsOptions.to_starting_strategy('config')
+        SupvisorsOptions.to_starting_strategy('configs')
     # test valid values
+    assert SupvisorsOptions.to_starting_strategy('config') == StartingStrategies.CONFIG
     assert SupvisorsOptions.to_starting_strategy('CONFIG') == StartingStrategies.CONFIG
     assert SupvisorsOptions.to_starting_strategy('LESS_LOADED') == StartingStrategies.LESS_LOADED
     assert SupvisorsOptions.to_starting_strategy('MOST_LOADED') == StartingStrategies.MOST_LOADED
 
 
+def test_statistics_type():
+    """ Test the conversion of a string to a pair of booleans for host and process statistics. """
+    error_message = common_error_message.format('stats_enabled')
+    # test invalid values
+    with pytest.raises(ValueError, match=error_message):
+        SupvisorsOptions.to_statistics_type('')
+    with pytest.raises(ValueError, match=error_message):
+        SupvisorsOptions.to_statistics_type('activated')
+    with pytest.raises(ValueError, match=error_message):
+        SupvisorsOptions.to_statistics_type('1,both')
+    # test valid values
+    assert SupvisorsOptions.to_statistics_type('OFF') == (False, False)
+    assert SupvisorsOptions.to_statistics_type('HOST') == (True, False)
+    assert SupvisorsOptions.to_statistics_type('PROCESS') == (False, True)
+    assert SupvisorsOptions.to_statistics_type('ALL') == (True, True)
+    assert SupvisorsOptions.to_statistics_type('HOST, PROCESS') == (True, True)
+    assert SupvisorsOptions.to_statistics_type('true, PROCESS') == (True, True)
+    assert SupvisorsOptions.to_statistics_type('OFF, host') == (True, False)
+    assert SupvisorsOptions.to_statistics_type('False, process, 0') == (False, True)
+
+
+def test_period():
+    """ Test the conversion of a string to a collecting period. """
+    error_message = common_error_message.format('stats_collecting_period')
+    # test invalid values
+    with pytest.raises(ValueError, match=error_message):
+        SupvisorsOptions.to_period('')
+    with pytest.raises(ValueError, match=error_message):
+        SupvisorsOptions.to_period('0.9,3600')
+    with pytest.raises(ValueError, match=error_message):
+        SupvisorsOptions.to_period('dummy')
+    with pytest.raises(ValueError, match=error_message):
+        SupvisorsOptions.to_period('0')
+    with pytest.raises(ValueError, match=error_message):
+        SupvisorsOptions.to_period('3601')
+    # test valid values
+    assert SupvisorsOptions.to_period('5') == 5
+    assert SupvisorsOptions.to_period('3.3') == 3.3
+    assert SupvisorsOptions.to_period('99.99') == 99.99
+
+
 def test_periods():
     """ Test the conversion of a string to a list of periods. """
     error_message = common_error_message.format('stats_periods')
@@ -261,17 +311,17 @@ def test_periods():
     with pytest.raises(ValueError, match='unexpected number of stats_periods: 4. maximum is 3'):
         SupvisorsOptions.to_periods('1, 2, 3, 4')
     with pytest.raises(ValueError, match=error_message):
-        SupvisorsOptions.to_periods('4,3600')
-    with pytest.raises(ValueError, match=error_message):
-        SupvisorsOptions.to_periods('5,3601')
+        SupvisorsOptions.to_periods('0.9,3600')
     with pytest.raises(ValueError, match=error_message):
-        SupvisorsOptions.to_periods('6, 3599')
+        SupvisorsOptions.to_periods('1,3600.1')
     with pytest.raises(ValueError, match=error_message):
         SupvisorsOptions.to_periods('90, none')
     # test valid values
     assert SupvisorsOptions.to_periods('5') == [5]
+    assert SupvisorsOptions.to_periods('3.3') == [3.3]
     assert SupvisorsOptions.to_periods('60,3600') == [60, 3600]
-    assert SupvisorsOptions.to_periods('720, 120,1800') == [120, 720, 1800]
+    assert SupvisorsOptions.to_periods('1.0, 3599') == [1.0, 3599]
+    assert SupvisorsOptions.to_periods('720, 120,1234.56') == [120, 720, 1234.56]
 
 
 def test_histo():
diff --git a/supvisors/tests/test_plot.py b/supvisors/tests/test_plot.py
index 498bef45..4992a482 100644
--- a/supvisors/tests/test_plot.py
+++ b/supvisors/tests/test_plot.py
@@ -33,8 +33,10 @@ def test_plot(supvisors):
     plot = StatisticsPlot(supvisors.logger)
     assert plot.ydata == {}
     # add series of data
+    plot.add_timeline([0, 1, 2])
     plot.add_plot('dummy_title_1', 'unit_1', [1, 2, 3])
     plot.add_plot('dummy_title_2', 'unit_2', [10, 20, 30])
+    assert plot.xdata == [0, 1, 2]
     assert plot.ydata == {('dummy_title_1', 'unit_1'): [1, 2, 3], ('dummy_title_2', 'unit_2'): [10, 20, 30]}
     # export image in buffer
     contents = StatsImage()
@@ -48,6 +50,7 @@ def test_plot_error(mocker, supvisors):
     plot = StatisticsPlot(supvisors.logger)
     assert plot.ydata == {}
     # add series of data
+    plot.add_timeline([0, 1, 2])
     plot.add_plot('dummy_title_1', 'unit_1', [1, 2, 3])
     plot.add_plot('dummy_title_2', 'unit_2', [10, 20, 30])
     assert plot.ydata == {('dummy_title_1', 'unit_1'): [1, 2, 3], ('dummy_title_2', 'unit_2'): [10, 20, 30]}
diff --git a/supvisors/tests/test_process.py b/supvisors/tests/test_process.py
index 7ac1d7db..588e80ff 100644
--- a/supvisors/tests/test_process.py
+++ b/supvisors/tests/test_process.py
@@ -305,10 +305,10 @@ def test_status_stopped_process(supvisors):
     assert process.stopped()
     assert not process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert not process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # test again with forced state
     event = {'state': ProcessStates.FATAL, 'identifier': '10.0.0.2', 'now': time(), 'spawnerr': ''}
     assert process.force_state(event)
@@ -316,22 +316,26 @@ def test_status_stopped_process(supvisors):
     assert process.stopped()
     assert not process.running()
     assert process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert not process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # STOPPED does not reset the forced state
     process.reset_forced_state(ProcessStates.STOPPED)
     assert process._state == ProcessStates.STOPPED
     assert process.stopped()
     assert not process.running()
     assert process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     # use empty param to reset the forced state
     process.reset_forced_state()
     assert process._state == ProcessStates.STOPPED
     assert process.stopped()
     assert not process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
 
 
 def test_status_backoff_process(supvisors):
@@ -342,10 +346,10 @@ def test_status_backoff_process(supvisors):
     assert not process.stopped()
     assert process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # test again with forced state
     event = {'state': ProcessStates.STOPPED, 'identifier': '10.0.0.1', 'now': time(), 'spawnerr': ''}
     assert process.force_state(event)
@@ -353,16 +357,18 @@ def test_status_backoff_process(supvisors):
     assert process.stopped()
     assert not process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert not process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # BACKOFF resets the forced state
     process.reset_forced_state(ProcessStates.BACKOFF)
     assert process._state == ProcessStates.BACKOFF
     assert not process.stopped()
     assert process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
 
 
 def test_status_running_process(supvisors):
@@ -373,10 +379,10 @@ def test_status_running_process(supvisors):
     assert not process.stopped()
     assert process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # test again with forced state
     # here, the stored event is more recent so the forced state is ignored
     event = {'state': ProcessStates.FATAL, 'identifier': '10.0.0.1', 'now': 0, 'spawnerr': ''}
@@ -385,10 +391,10 @@ def test_status_running_process(supvisors):
     assert not process.stopped()
     assert process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
 
 
 def test_status_stopping_process(supvisors):
@@ -399,10 +405,10 @@ def test_status_stopping_process(supvisors):
     assert not process.stopped()
     assert not process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert not process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # test again with forced state
     event = {'state': ProcessStates.STOPPED, 'identifier': '10.0.0.1', 'now': time(), 'spawnerr': ''}
     assert process.force_state(event)
@@ -410,16 +416,18 @@ def test_status_stopping_process(supvisors):
     assert process.stopped()
     assert not process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert not process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # STOPPING resets the forced state
     process.reset_forced_state(ProcessStates.STOPPING)
     assert process._state == ProcessStates.STOPPING
     assert not process.stopped()
     assert not process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
 
 
 def test_status_fatal_process(supvisors):
@@ -430,10 +438,10 @@ def test_status_fatal_process(supvisors):
     assert process.stopped()
     assert not process.running()
     assert process.crashed()
+    assert process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert not process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # test again with forced state
     event = {'state': ProcessStates.STOPPED, 'identifier': '10.0.0.1', 'now': time(), 'spawnerr': ''}
     assert process.force_state(event)
@@ -441,16 +449,18 @@ def test_status_fatal_process(supvisors):
     assert process.stopped()
     assert not process.running()
     assert not process.crashed()
+    assert process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert not process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # FATAL resets the forced state
     process.reset_forced_state(ProcessStates.FATAL)
     assert process._state == ProcessStates.FATAL
     assert process.stopped()
     assert not process.running()
     assert process.crashed()
+    assert process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
 
 
 def test_status_exited_process(supvisors):
@@ -463,19 +473,19 @@ def test_status_exited_process(supvisors):
     assert process.stopped()
     assert not process.running()
     assert not process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert not process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # test with unexpected_exit
     process.expected_exit = False
     assert process.stopped()
     assert not process.running()
     assert process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert not process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # test again with forced state
     event = {'state': ProcessStates.FATAL, 'identifier': '10.0.0.1', 'now': time(), 'spawnerr': ''}
     assert process.force_state(event)
@@ -483,16 +493,18 @@ def test_status_exited_process(supvisors):
     assert process.stopped()
     assert not process.running()
     assert process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
     assert not process.running_on('10.0.0.1')
     assert not process.running_on('10.0.0.2')
-    assert not process.pid_running_on('10.0.0.1')
-    assert not process.pid_running_on('10.0.0.2')
     # EXITED resets the forced state
     process.reset_forced_state(ProcessStates.EXITED)
     assert process._state == ProcessStates.EXITED
     assert process.stopped()
     assert not process.running()
     assert process.crashed()
+    assert not process.crashed('10.0.0.1')
+    assert not process.crashed('10.0.0.2')
 
 
 def test_process_conflicting(supvisors):
diff --git a/supvisors/tests/test_publisherinterface.py b/supvisors/tests/test_publisherinterface.py
deleted file mode 100644
index b3d54876..00000000
--- a/supvisors/tests/test_publisherinterface.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# ======================================================================
-# Copyright 2022 Julien LE CLEACH
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-# ======================================================================
-
-import pytest
-
-from supvisors.publisherinterface import *
-
-
-@pytest.fixture
-def zmq_import():
-    return pytest.importorskip('zmq')
-
-
-@pytest.fixture
-def zmq_fail_import(mocker):
-    """ Mock ImportError on optional lxml if installed to force ElementTree testing. """
-    mocker.patch.dict('sys.modules', {'zmq': None})
-
-
-def test_interface():
-    """ Test the EventPublisherInterface abstract class. """
-    intf = EventPublisherInterface()
-    with pytest.raises(NotImplementedError):
-        intf.close()
-    with pytest.raises(NotImplementedError):
-        intf.send_supvisors_status({})
-    with pytest.raises(NotImplementedError):
-        intf.send_instance_status({})
-    with pytest.raises(NotImplementedError):
-        intf.send_application_status({})
-    with pytest.raises(NotImplementedError):
-        intf.send_process_event('10.0.0.1', {})
-    with pytest.raises(NotImplementedError):
-        intf.send_process_status({})
-
-
-def test_create_external_publisher_none(supvisors):
-    """ Test the create_external_publisher function with no event link selected. """
-    assert create_external_publisher(supvisors) is None
-
-
-def test_create_external_publisher_zmq_fail(zmq_import, zmq_fail_import, supvisors):
-    """ Test the create_external_publisher function with zmq event link but not installed. """
-    supvisors.options.event_link = EventLinks.ZMQ
-    # test inclusion of Supvisors into Supervisor
-    assert create_external_publisher(supvisors) is None
-
-
-def test_create_external_publisher_zmq(zmq_import, supvisors):
-    """ Test the make_supvisors_rpcinterface function. """
-    from supvisors.supvisorszmq import EventPublisher
-    supvisors.options.event_link = EventLinks.ZMQ
-    # test inclusion of Supvisors into Supervisor
-    instance = create_external_publisher(supvisors)
-    assert isinstance(instance, EventPublisher)
diff --git a/supvisors/tests/test_rpcinterface.py b/supvisors/tests/test_rpcinterface.py
index 5e494aeb..3bebb598 100644
--- a/supvisors/tests/test_rpcinterface.py
+++ b/supvisors/tests/test_rpcinterface.py
@@ -78,6 +78,7 @@ def test_instance_info(rpc):
     # test with known identifier
     expected = {'identifier': '10.0.0.1', 'node_name': '10.0.0.1', 'port': 65000, 'loading': 0, 'local_time': 0,
                 'remote_time': 0, 'sequence_counter': 0, 'statecode': 0, 'statename': 'UNKNOWN',
+                'process_failure': False,
                 'fsm_statecode': 4, 'fsm_statename': 'CONCILIATION', 'starting_jobs': False, 'stopping_jobs': True}
     assert rpc.get_instance_info('10.0.0.1') == expected
     # test with unknown identifier
diff --git a/supvisors/tests/test_statemachine.py b/supvisors/tests/test_statemachine.py
index e6f61fbc..ba21f20f 100644
--- a/supvisors/tests/test_statemachine.py
+++ b/supvisors/tests/test_statemachine.py
@@ -210,10 +210,10 @@ def test_master_operation_state(mocker, supvisors_ctx):
     result = state.next()
     assert result == SupvisorsStates.OPERATION
     mocked_stop.return_value = False
-    # create address context
-    for node_name in supvisors_ctx.supvisors_mapper.instances:
-        status = SupvisorsInstanceStatus(node_name, supvisors_ctx)
-        supvisors_ctx.context.instances[node_name] = status
+    # create instance context
+    for instance_id in supvisors_ctx.supvisors_mapper.instances.values():
+        status = SupvisorsInstanceStatus(instance_id, supvisors_ctx)
+        supvisors_ctx.context.instances[instance_id.identifier] = status
     # no starting or stopping is in progress
     # stay in OPERATION if no conflict
     mocked_conflict = mocker.patch.object(supvisors_ctx.context, 'conflicting', return_value=False)
diff --git a/supvisors/tests/test_statscollector.py b/supvisors/tests/test_statscollector.py
index 45a198a8..ac2cb323 100644
--- a/supvisors/tests/test_statscollector.py
+++ b/supvisors/tests/test_statscollector.py
@@ -17,8 +17,8 @@
 # limitations under the License.
 # ======================================================================
 
-import multiprocessing
-import time
+from threading import Thread
+from unittest.mock import call, Mock
 
 import pytest
 
@@ -27,11 +27,36 @@
 pytest.importorskip('psutil', reason='cannot test as optional psutil is not installed')
 
 
+@pytest.fixture
+def queues():
+    pid_queue = mp.SimpleQueue()
+    stats_queue = mp.SimpleQueue()
+    return pid_queue, stats_queue
+
+
+@pytest.fixture
+def collected_processes(queues):
+    return CollectedProcesses(queues[1], 5, os.getpid())
+
+
 def test_instant_cpu_statistics():
     """ Test the instant CPU statistics. """
-    stats = list(instant_cpu_statistics())
+    # measurement at t0
+    work1, idle1 = instant_cpu_statistics()
+    # do some work
+    sleep(1)
+    # measurement at t0+1
+    work2, idle2 = instant_cpu_statistics()
+    # jiffies should be increasing
+    assert work2 >= work1
+    assert idle2 >= idle1
+
+
+def test_instant_all_cpu_statistics():
+    """ Test the instant CPU statistics. """
+    stats = instant_all_cpu_statistics()
     # test number of results (number of cores + average)
-    assert len(stats) == multiprocessing.cpu_count() + 1
+    assert len(stats) == mp.cpu_count() + 1
     # test average value
     total_work = total_idle = 0
     for cpu in stats[1:]:
@@ -39,8 +64,8 @@ def test_instant_cpu_statistics():
         work, idle = cpu
         total_work += work
         total_idle += idle
-    assert pytest.approx(total_work / multiprocessing.cpu_count()) == stats[0][0]
-    assert pytest.approx(total_idle / multiprocessing.cpu_count()) == stats[0][1]
+    assert pytest.approx(total_work / mp.cpu_count()) == stats[0][0]
+    assert pytest.approx(total_idle / mp.cpu_count()) == stats[0][1]
 
 
 def test_instant_memory_statistics():
@@ -71,12 +96,39 @@ def test_instant_io_statistics():
     assert stats['lo'][0] == stats['lo'][1]
 
 
+def test_instant_host_statistics():
+    """ Test the instant host statistics. """
+    stats = instant_host_statistics()
+    # check result
+    assert len(stats) == 4
+    #  check time (current is greater)
+    assert time() > stats['now']
+    # check cpu jiffies
+    cpu_stats = stats['cpu']
+    assert len(cpu_stats) == mp.cpu_count() + 1
+    for cpu in cpu_stats:
+        assert len(cpu) == 2
+        for value in cpu:
+            assert type(value) is float
+    # check memory
+    mem_stats = stats['mem']
+    assert type(mem_stats) is float
+    assert mem_stats >= 0
+    assert mem_stats < 100
+    # check io
+    io_stats = stats['io']
+    for intf, io_bytes in io_stats.items():
+        assert type(intf) is str
+        assert len(io_bytes) == 2
+        for value in io_bytes:
+            assert type(value) is int
+
+
 def test_instant_process_statistics(mocker):
-    """ Test the instant process statistics. """
-    mocked_process = mocker.patch('psutil.Process.children', return_value=[Process(os.getpid())])
+    """ Test the instant process statistics without children. """
+    this_process = psutil.Process(os.getpid())
     # check with existing PID and no children requested
-    work, memory = instant_process_statistics(os.getpid(), False)
-    assert not mocked_process.called
+    work, memory = instant_process_statistics(this_process, False)
     # test that a pair is returned with values in [0;100]
     # test cpu value
     assert type(work) is float
@@ -86,8 +138,31 @@ def test_instant_process_statistics(mocker):
     assert type(memory) is float
     assert memory >= 0
     assert memory <= 100
+    # check with exception PID
+    mocker.patch.object(this_process, 'as_dict', side_effect=psutil.NoSuchProcess(os.getpid()))
+    assert instant_process_statistics(this_process, False) is None
+
+
+def test_instant_process_statistics_children(mocker):
+    """ Test the instant process statistics with children requested. """
+    this_process = psutil.Process()
+    this_process_as_child = psutil.Process()
+    mocked_process = mocker.patch('psutil.Process.children', return_value=[this_process_as_child])
     # check with existing PID and children requested
-    work, memory = instant_process_statistics(os.getpid())
+    work, memory = instant_process_statistics(this_process)
+    assert mocked_process.called
+    # test that a pair is returned with values in [0;100]
+    # test cpu value
+    assert type(work) is float
+    assert work >= 0
+    assert work <= 100
+    # test mem value
+    assert type(memory) is float
+    assert memory >= 0
+    assert memory <= 100
+    # check with existing PID, children requested but children dead
+    mocker.patch.object(this_process_as_child, 'as_dict', side_effect=psutil.NoSuchProcess(os.getpid()))
+    work, memory = instant_process_statistics(this_process)
     assert mocked_process.called
     # test that a pair is returned with values in [0;100]
     # test cpu value
@@ -98,53 +173,171 @@ def test_instant_process_statistics(mocker):
     assert type(memory) is float
     assert memory >= 0
     assert memory <= 100
-    # check handling of non-existing PID
-    work, memory = instant_process_statistics(-1)
-    assert work == 0
-    assert memory == 0
 
 
-def test_instant_statistics():
-    """ Test the instant global statistics. """
-    stats = instant_statistics([('myself', os.getpid())])
-    # check result
-    assert len(stats) == 5
-    date, cpu_stats, mem_stats, io_stats, proc_stats = stats
-    #  check time (current is greater)
-    assert time() > date
-    # check cpu jiffies
-    assert len(list(cpu_stats)) == multiprocessing.cpu_count() + 1
-    for cpu in cpu_stats:
-        assert len(cpu) == 2
-        for value in cpu:
-            assert type(value) is float
-    # check memory
-    assert type(mem_stats) is float
-    assert mem_stats >= 0
-    assert mem_stats < 100
-    # check io
-    for intf, io_bytes in io_stats.items():
-        assert type(intf) is str
-        assert len(io_bytes) == 2
-        for value in io_bytes:
-            assert type(value) is int
-    # check process stats
-    assert list(proc_stats.keys()) == ['myself', 'supervisord']
-    # check first entry
-    values = proc_stats['myself']
-    assert len(values) == 2
-    assert values[0] == os.getpid()
-    assert len(values[1]) == 2
-    for value in values[1]:
-        assert type(value) is float
-        assert value >= 0
-        assert value <= 100
-    # check supervisord entry
-    values = proc_stats['supervisord']
-    assert len(values) == 2
-    assert values[0] == os.getpid()
-    assert len(values[1]) == 2
-    for value in values[1]:
-        assert type(value) is float
-        assert value >= 0
-        assert value <= 100
+def test_collected_processes_creation(queues, collected_processes):
+    """ Test the CollectedProcesses creation. """
+    assert collected_processes.stats_queue is queues[1]
+    assert collected_processes.period == 5
+    assert collected_processes.processes == []
+    assert collected_processes.nb_cores == mp.cpu_count()
+    assert sorted(collected_processes.supervisor_process.keys()) == ['collector', 'last', 'supervisor']
+    assert collected_processes.supervisor_process['last'] == 0
+    assert collected_processes.supervisor_process['supervisor'].pid == os.getpid()
+    assert collected_processes.supervisor_process['collector'].pid == os.getpid()
+
+
+def test_collected_processes_update_process_list(mocker, queues, collected_processes):
+    """ Test the CollectedProcesses.update_process_list method. """
+    mocker.patch('supvisors.statscollector.time', return_value=7777)
+    mocked_process = mocker.patch.object(psutil, 'Process', side_effect=psutil.NoSuchProcess(1234))
+    # first try: not found and process does not exist
+    collected_processes.update_process_list('dummy_proc', 1234)
+    assert mocked_process.call_args_list == [call(1234)]
+    assert queues[1].empty()
+    assert collected_processes.processes == []
+    mocked_process.reset_mock()
+    # second try: not found but process exists
+    mocked_process.side_effect = lambda x: Mock(pid=x)
+    collected_processes.update_process_list('dummy_proc', 1234)
+    assert mocked_process.call_args_list == [call(1234)]
+    assert queues[1].empty()
+    assert len(collected_processes.processes) == 1
+    process = collected_processes.processes[0]
+    assert process['namespec'] == 'dummy_proc'
+    assert process['last'] == 0
+    assert process['process'].pid == 1234
+    mocked_process.reset_mock()
+    # third try: update with same pid
+    collected_processes.update_process_list('dummy_proc', 1234)
+    assert not mocked_process.called
+    assert queues[1].empty()
+    assert len(collected_processes.processes) == 1
+    process = collected_processes.processes[0]
+    assert process['namespec'] == 'dummy_proc'
+    assert process['last'] == 0
+    assert process['process'].pid == 1234
+    mocked_process.reset_mock()
+    # fourth try: update with a different positive pid
+    collected_processes.update_process_list('dummy_proc', 4321)
+    assert mocked_process.call_args_list == [call(4321)]
+    assert not queues[1].empty()
+    assert queues[1].get() == {'namespec': 'dummy_proc', 'pid': 0, 'now': 7777}
+    assert len(collected_processes.processes) == 1
+    process = collected_processes.processes[0]
+    assert process['namespec'] == 'dummy_proc'
+    assert process['last'] == 0
+    assert process['process'].pid == 4321
+    mocked_process.reset_mock()
+    # fifth try: update with a different zero pid
+    collected_processes.update_process_list('dummy_proc', 0)
+    assert not mocked_process.called
+    assert not queues[1].empty()
+    assert queues[1].get() == {'namespec': 'dummy_proc', 'pid': 0, 'now': 7777}
+    assert collected_processes.processes == []
+
+
+def test_collected_processes_collect_supervisor(mocker, queues, collected_processes):
+    """ Test the CollectedProcesses.collect_supervisor method. """
+    mocked_time = mocker.patch('supvisors.statscollector.time', return_value=4)
+    # first try with not enough time to trigger the collection
+    collected_processes.collect_supervisor()
+    assert queues[1].empty()
+    assert collected_processes.supervisor_process['last'] == 0
+    # second try with enough time to trigger the collection
+    mocked_time.return_value = 5
+    collected_processes.collect_supervisor()
+    assert not queues[1].empty()
+    supervisor_stats = queues[1].get()
+    assert sorted(supervisor_stats.keys()) == ['cpu', 'namespec', 'nb_cores', 'now', 'pid', 'proc_memory', 'proc_work']
+    assert supervisor_stats['namespec'] == 'supervisord'
+    assert supervisor_stats['nb_cores'] == mp.cpu_count()
+    assert supervisor_stats['now'] == 5
+    assert supervisor_stats['pid'] == os.getpid()
+    assert 0 < supervisor_stats['proc_memory'] < 100
+    assert supervisor_stats['proc_work'] > 0
+    assert len(supervisor_stats['cpu']) == 2
+    assert collected_processes.supervisor_process['last'] == 5
+
+
+def test_collected_processes_collect_recent_process(mocker, queues, collected_processes):
+    """ Test the CollectedProcesses.collect_recent_process method. """
+    mocked_time = mocker.patch('supvisors.statscollector.time', return_value=22)
+    # first try with no process to collect
+    collected_processes.collect_recent_process()
+    assert queues[1].empty()
+    # second try with processes but not enough time to trigger the collection
+    process = psutil.Process()
+    collected_processes.processes = [{'namespec': 'dummy_1', 'last': 21, 'process': process},
+                                     {'namespec': 'dummy_2', 'last': 18, 'process': process}]
+    collected_processes.collect_recent_process()
+    assert queues[1].empty()
+    assert collected_processes.processes == [{'namespec': 'dummy_1', 'last': 21, 'process': process},
+                                             {'namespec': 'dummy_2', 'last': 18, 'process': process}]
+    # third try with enough time to trigger the collection on the first element of the list
+    mocked_time.return_value = 24
+    collected_processes.collect_recent_process()
+    assert not queues[1].empty()
+    supervisor_stats = queues[1].get()
+    assert sorted(supervisor_stats.keys()) == ['cpu', 'namespec', 'now', 'pid', 'proc_memory', 'proc_work']
+    assert supervisor_stats['namespec'] == 'dummy_2'
+    assert supervisor_stats['now'] == 24
+    assert supervisor_stats['pid'] == os.getpid()
+    assert 0 < supervisor_stats['proc_memory'] < 100
+    assert supervisor_stats['proc_work'] > 0
+    assert len(supervisor_stats['cpu']) == 2
+    # check that the list has rotated
+    assert collected_processes.processes == [{'namespec': 'dummy_2', 'last': 24, 'process': process},
+                                             {'namespec': 'dummy_1', 'last': 21, 'process': process}]
+    # fourth try with enough time to trigger the collection on the first element of the list
+    # but with stopped process
+    mocker.patch('supvisors.statscollector.instant_process_statistics', return_value=None)
+    mocked_time.return_value = 27
+    collected_processes.collect_recent_process()
+    assert not queues[1].empty()
+    supervisor_stats = queues[1].get()
+    assert sorted(supervisor_stats.keys()) == ['namespec', 'now', 'pid']
+    assert supervisor_stats['namespec'] == 'dummy_1'
+    assert supervisor_stats['now'] == 27
+    assert supervisor_stats['pid'] == 0
+    # check that dummy_1 has been removed from the list
+    assert collected_processes.processes == [{'namespec': 'dummy_2', 'last': 24, 'process': process}]
+
+
+def test_collect_process_statistics(mocker, queues):
+    """ Test the collect_process_statistics main loop of the process collector. """
+    mocked_processes = Mock(spec=CollectedProcesses)
+    mocked_processes.collect_recent_process.return_value = False
+    mocked_constructor = mocker.patch('supvisors.statscollector.CollectedProcesses', return_value=mocked_processes)
+    # pre-fill the sending queue
+    queues[0].put(('dummy_1', 123))
+    queues[0].put(('dummy_2', 456))
+    def terminate():
+        sleep(1)
+        queues[0].put(None)
+    Thread(target=terminate).start()
+    # trigger the main loop
+    collect_process_statistics(queues[0], queues[1], 5, 777)
+    assert mocked_constructor.call_args_list == [call(queues[1], 5, 777)]
+    assert mocked_processes.update_process_list.call_args_list == [call('dummy_1', 123), call('dummy_2', 456)]
+    # due to the multi-threading aspect, impossible to predict the exact number of calls
+    assert mocked_processes.collect_supervisor.call_count > 1
+    assert mocked_processes.collect_recent_process.call_count > 1
+
+
+def test_process_statistics_collector(mocker):
+    """ Test the ProcessStatisticsCollector class. """
+    mocked_process = Mock(spec=mp.Process)
+    mocked_creation = mocker.patch('multiprocessing.Process', return_value=mocked_process)
+    # test creation
+    collector = ProcessStatisticsCollector(10)
+    assert collector.period == 10
+    assert type(collector.pid_queue) ==  mp.queues.SimpleQueue
+    assert type(collector.stats_queue) ==  mp.queues.SimpleQueue
+    assert collector.process is None
+    # test thread starting
+    collector.start()
+    assert mocked_creation.call_args_list == [call(target=collect_process_statistics,
+                                                   args=(collector.pid_queue, collector.stats_queue, 10, os.getpid()),
+                                                   daemon=True)]
+    assert mocked_process.start.call_args_list == [call()]
diff --git a/supvisors/tests/test_statscompiler.py b/supvisors/tests/test_statscompiler.py
index 7d31811c..092fefe9 100644
--- a/supvisors/tests/test_statscompiler.py
+++ b/supvisors/tests/test_statscompiler.py
@@ -18,7 +18,9 @@
 # ======================================================================
 
 import multiprocessing
-from unittest.mock import call
+import os
+import time
+from unittest.mock import call, Mock
 
 import pytest
 
@@ -43,8 +45,8 @@ def test_cpu_statistics():
 def test_cpu_total_work():
     """ Test the CPU total work between 2 dates. """
     # take 2 instant cpu statistics
-    ref_stats = [(83.31, 305.4)] * 2
-    last_stats = [(83.41, 306.3)] * 2
+    ref_stats = (83.31, 305.4)
+    last_stats = (83.41, 306.3)
     total_work = cpu_total_work(last_stats, ref_stats)
     # total work is the sum of jiffies spent on cpu all
     assert pytest.approx(total_work) == 1
@@ -63,374 +65,606 @@ def test_io_statistics():
     assert stats == {'lo': (8, 8), 'eth0': (7, 1)}
 
 
-def test_cpu_process_statistics():
-    """ Test the CPU of the process between 2 dates. """
-    stats = cpu_process_statistics(50, 20, 100)
-    assert type(stats) is float
-    assert pytest.approx(stats) == 30
-
-
-def test_statistics():
-    """ Test the global statistics between 2 dates. """
-    ref_stats = (1000, [(25, 400), (25, 125), (15, 150)], 65, {'eth0': (2000, 200), 'lo': (5000, 5000)},
-                 {'myself': (26088, (0.15, 1.85))})
-    last_stats = (1002, [(45, 700), (50, 225), (40, 250)], 67.7, {'eth0': (2768, 456), 'lo': (6024, 6024)},
-                  {'myself': (26088, (1.75, 1.9))})
-    stats = statistics(last_stats, ref_stats)
-    # check result
-    assert len(stats) == 5
-    date, cpu_stats, mem_stats, io_stats, proc_stats = stats
-    # check date
-    assert date == 1002
-    # check cpu
-    assert cpu_stats == [6.25, 20.0, 20.0]
-    # check memory
-    assert pytest.approx(mem_stats) == 67.7
-    # check io
-    assert io_stats == {'lo': (4, 4), 'eth0': (3, 1)}
-    # check process stats
-    assert proc_stats == {('myself', 26088): (0.5, 1.9)}
-
-
-@pytest.fixture
-def statistics_instance(supvisors):
-    # testing with period 12 and history depth 2
-    return StatisticsInstance(12, 2, supvisors.logger)
-
-
-def test_stats_create(statistics_instance):
-    """ Test the initialization of an instance. """
-    # check attributes
-    assert statistics_instance.period == 2
-    assert statistics_instance.depth == 2
-    assert statistics_instance.counter == -1
-    assert statistics_instance.ref_stats is None
-    assert type(statistics_instance.cpu) is list
-    assert not statistics_instance.cpu
-    assert type(statistics_instance.mem) is list
-    assert not statistics_instance.mem
-    assert type(statistics_instance.io) is dict
-    assert not statistics_instance.io
-    assert type(statistics_instance.proc) is dict
-    assert not statistics_instance.proc
-
-
-def test_clear(statistics_instance):
-    """ Test the clearance of an instance. """
-    # change values
-    statistics_instance.counter = 28
-    statistics_instance.ref_stats = ('dummy', 0)
-    statistics_instance.cpu = [13.2, 14.8]
-    statistics_instance.mem = [56.4, 71.3, 68.9]
-    statistics_instance.io = {'eth0': (123465, 654321), 'lo': (321, 321)}
-    statistics_instance.proc = {('myself', 5888): (25.0, 12.5)}
-    # check clearance
-    statistics_instance.clear()
-    assert statistics_instance.period == 2
-    assert statistics_instance.depth == 2
-    assert statistics_instance.counter == -1
-    assert statistics_instance.ref_stats is None
-    assert type(statistics_instance.cpu) is list
-    assert not statistics_instance.cpu
-    assert type(statistics_instance.mem) is list
-    assert not statistics_instance.mem
-    assert type(statistics_instance.io) is dict
-    assert not statistics_instance.io
-    assert type(statistics_instance.proc) is dict
-    assert not statistics_instance.proc
-
-
-def test_find_process_stats(statistics_instance):
-    """ Test the search method for process statistics. """
-    # change values
-    statistics_instance.proc = {('the_other', 1234): (1.5, 2.4), ('myself', 5888): (25.0, 12.5)}
-    # test find method with wrong argument
-    assert statistics_instance.find_process_stats('someone') is None
-    # test find method with correct argument
-    stats = statistics_instance.find_process_stats('myself')
-    assert stats == (25.0, 12.5)
-
-
-def test_trunc_depth(statistics_instance):
+def test_trunc_depth():
     """ Test the history depth. """
     # test that the trunc_depth method does nothing when less than 2 elements in list
     test_list = [1, 2]
-    statistics_instance.trunc_depth(test_list)
+    trunc_depth(test_list, 5)
     assert test_list == [1, 2]
     # test that the trunc_depth method keeps only the last 5 elements in list
     test_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
-    statistics_instance.trunc_depth(test_list)
-    assert test_list == [9, 10]
+    trunc_depth(test_list, 5)
+    assert test_list == [6, 7, 8, 9, 10]
 
 
-def test_push_cpu_stats(statistics_instance):
-    """ Test the storage of the CPU instant statistics. """
+# Host Statistics part
+@pytest.fixture
+def host_statistics_instance(supvisors):
+    # testing with period 12 and history depth 2
+    return HostStatisticsInstance('10.0.0.1', 12, 2, supvisors.logger)
+
+
+def test_host_statistics_instance_creation(host_statistics_instance, supvisors):
+    """ Test the creation of HostStatisticsInstance. """
+    assert host_statistics_instance.logger is supvisors.logger
+    assert host_statistics_instance.identifier == '10.0.0.1'
+    assert host_statistics_instance.period == 12
+    assert host_statistics_instance.depth == 2
+    assert host_statistics_instance.ref_stats == {}
+    assert host_statistics_instance.ref_start_time == 0.0
+    assert host_statistics_instance.times == []
+    assert host_statistics_instance.cpu == []
+    assert host_statistics_instance.mem == []
+    assert host_statistics_instance.io == {}
+
+
+def test_host_statistics_instance_push_times(host_statistics_instance):
+    """ Test the HostStatisticsInstance._push_times_stats method. """
+    host_statistics_instance._push_times_stats(1.0)
+    assert host_statistics_instance.times == [1.0]
+    host_statistics_instance._push_times_stats(2.5)
+    assert host_statistics_instance.times == [1.0, 2.5]
+    host_statistics_instance._push_times_stats(3.8)
+    assert host_statistics_instance.times == [2.5, 3.8]
+    host_statistics_instance._push_times_stats(5.7)
+    assert host_statistics_instance.times == [3.8, 5.7]
+
+
+def test_host_statistics_instance_push_cpu(host_statistics_instance):
+    """ Test the HostStatisticsInstance._push_cpu_stats method. """
     cpu_stats = [12.1, 16.0]
     # test adding CPU stats on non-initialized structure
     # should not happen as protected by the upper call of push_statistics
-    statistics_instance._push_cpu_stats(cpu_stats)
-    assert not statistics_instance.cpu
+    host_statistics_instance._push_cpu_stats(cpu_stats)
+    assert not host_statistics_instance.cpu
     # init internal structure and retry
-    statistics_instance.cpu = [[] for _ in cpu_stats]
-    statistics_instance._push_cpu_stats(cpu_stats)
-    assert statistics_instance.cpu == [[12.1], [16.0]]
+    host_statistics_instance.cpu = [[] for _ in cpu_stats]
+    host_statistics_instance._push_cpu_stats(cpu_stats)
+    assert host_statistics_instance.cpu == [[12.1], [16.0]]
     # again: list increases
     cpu_stats = [8.7, 14.6]
-    statistics_instance._push_cpu_stats(cpu_stats)
-    assert statistics_instance.cpu == [[12.1, 8.7], [16.0, 14.6]]
+    host_statistics_instance._push_cpu_stats(cpu_stats)
+    assert host_statistics_instance.cpu == [[12.1, 8.7], [16.0, 14.6]]
     # again: list rotates due to history depth at 2
     cpu_stats = [11.9, 5.5]
-    statistics_instance._push_cpu_stats(cpu_stats)
-    assert statistics_instance.cpu == [[8.7, 11.9], [14.6, 5.5]]
+    host_statistics_instance._push_cpu_stats(cpu_stats)
+    assert host_statistics_instance.cpu == [[8.7, 11.9], [14.6, 5.5]]
 
 
-def test_push_mem_stats(statistics_instance):
-    """ Test the storage of the MEM instant statistics. """
+def test_host_statistics_instance_push_mem(host_statistics_instance):
+    """ Test the HostStatisticsInstance._push_mem_stats method. """
     # MEM stats internal structure is initialized (simple list)
-    statistics_instance._push_mem_stats(12.1)
-    assert statistics_instance.mem == [12.1]
+    host_statistics_instance._push_mem_stats(12.1)
+    assert host_statistics_instance.mem == [12.1]
     # again: list increases
-    statistics_instance._push_mem_stats(8.7)
-    assert statistics_instance.mem == [12.1, 8.7]
+    host_statistics_instance._push_mem_stats(8.7)
+    assert host_statistics_instance.mem == [12.1, 8.7]
     # again: list rotates due to history depth at 2
-    statistics_instance._push_mem_stats(11.9)
-    assert statistics_instance.mem == [8.7, 11.9]
+    host_statistics_instance._push_mem_stats(11.9)
+    assert host_statistics_instance.mem == [8.7, 11.9]
 
 
-def test_push_io_stats(statistics_instance):
+def test_host_statistics_instance_push_io(host_statistics_instance):
     """ Test the storage of the IO instant statistics. """
     io_stats = {'eth0': (1024, 2000), 'lo': (500, 500)}
     # first time: structures are created
-    statistics_instance._push_io_stats(io_stats)
-    assert statistics_instance.io == {'eth0': ([1024], [2000]), 'lo': ([500], [500])}
+    host_statistics_instance._push_io_stats(io_stats)
+    assert host_statistics_instance.io == {'eth0': ([1024], [2000]), 'lo': ([500], [500])}
     # again: list increases
     io_stats = {'eth0': (1250, 2200), 'lo': (620, 620)}
-    statistics_instance._push_io_stats(io_stats)
-    assert statistics_instance.io == {'eth0': ([1024, 1250], [2000, 2200]), 'lo': ([500, 620], [500, 620])}
+    host_statistics_instance._push_io_stats(io_stats)
+    assert host_statistics_instance.io == {'eth0': ([1024, 1250], [2000, 2200]), 'lo': ([500, 620], [500, 620])}
     # again: list rotates due to history depth at 2
     io_stats = {'eth0': (2048, 2512), 'lo': (756, 756)}
-    statistics_instance._push_io_stats(io_stats)
-    assert statistics_instance.io == {'eth0': ([1250, 2048], [2200, 2512]), 'lo': ([620, 756], [620, 756])}
+    host_statistics_instance._push_io_stats(io_stats)
+    assert host_statistics_instance.io == {'eth0': ([1250, 2048], [2200, 2512]), 'lo': ([620, 756], [620, 756])}
     # test obsolete and new interface
     io_stats = {'eth1': (3072, 2768), 'lo': (1780, 1780)}
-    statistics_instance._push_io_stats(io_stats)
-    assert statistics_instance.io == {'eth1': ([3072], [2768]), 'lo': ([756, 1780], [756, 1780])}
+    host_statistics_instance._push_io_stats(io_stats)
+    assert host_statistics_instance.io == {'eth1': ([3072], [2768]), 'lo': ([756, 1780], [756, 1780])}
+
+
+def test_host_statistics_instance_integrate(host_statistics_instance):
+    """ Test the host statistics integration between 2 instants. """
+    host_statistics_instance.ref_start_time = 500
+    host_statistics_instance.ref_stats = {'now': 1000,
+                                          'cpu': [(25, 400), (25, 125), (15, 150)],
+                                          'mem': 65,
+                                          'io': {'eth0': (2000, 200), 'lo': (5000, 5000)}}
+    last_stats = {'now': 1002,
+                  'cpu': [(45, 700), (50, 225), (40, 250)],
+                  'mem': 67.7,
+                  'io': {'eth0': (2768, 456), 'lo': (6024, 6024)}}
+    stats = host_statistics_instance.integrate(last_stats)
+    # check result
+    assert len(stats) == 4
+    uptime, cpu_stats, mem_stats, io_stats = stats
+    # check date
+    assert uptime == 502
+    # check cpu
+    assert cpu_stats == [6.25, 20.0, 20.0]
+    # check memory
+    assert pytest.approx(mem_stats) == 67.7
+    # check io
+    assert io_stats == {'lo': (4, 4), 'eth0': (3, 1)}
 
 
-def test_push_process_stats(statistics_instance):
-    """ Test the storage of the process instant statistics. """
-    proc_stats = {('myself', 118612): (0.15, 1.85), ('other', 7754): (15.4, 12.2)}
-    statistics_instance._push_process_stats(proc_stats)
-    assert statistics_instance.proc == {('myself', 118612): ([0.15], [1.85]), ('other', 7754): ([15.4], [12.2])}
-    # again: list increases
-    proc_stats = {('myself', 118612): (1.9, 1.93), ('other', 7754): (14.9, 12.8)}
-    statistics_instance._push_process_stats(proc_stats)
-    assert statistics_instance.proc == {('myself', 118612): ([0.15, 1.9], [1.85, 1.93]),
-                                        ('other', 7754): ([15.4, 14.9], [12.2, 12.8])}
-    # again: list rotates due to history depth at 2
-    proc_stats = {('myself', 118612): (2.47, 2.04), ('other', 7754): (6.5, 13.0)}
-    statistics_instance._push_process_stats(proc_stats)
-    assert statistics_instance.proc == {('myself', 118612): ([1.9, 2.47], [1.93, 2.04]),
-                                        ('other', 7754): ([14.9, 6.5], [12.8, 13.0])}
-    # test obsolete and new processes (here other is just restarted - new pid)
-    proc_stats = {('myself', 118612): (0.15, 1.85), ('other', 826): (1.89, 2.67)}
-    statistics_instance._push_process_stats(proc_stats)
-    assert statistics_instance.proc == {('myself', 118612): ([2.47, 0.15], [2.04, 1.85]),
-                                        ('other', 826): ([1.89], [2.67])}
-
-
-def test_push_statistics(mocker, statistics_instance):
-    """ Test the storage of the instant statistics. """
-    mocked_stats = mocker.patch('supvisors.statscompiler.statistics')
-    mocked_cpu = mocker.patch.object(statistics_instance, '_push_cpu_stats')
-    mocked_mem = mocker.patch.object(statistics_instance, '_push_mem_stats')
-    mocked_io = mocker.patch.object(statistics_instance, '_push_io_stats')
-    mocked_proc = mocker.patch.object(statistics_instance, '_push_process_stats')
-    # push first set of measures
-    stats1 = 'stats 1'
-    mocked_stats.return_value = (8.5, 'cpu_stats 1', 76.1, 'io_stats 1', 'proc_stats 1')
-    statistics_instance.push_statistics(stats1)
+def test_host_statistics_instance_push_statistics(mocker, host_statistics_instance):
+    """ Test the reception of new host statistics. """
+    mocked_stats = mocker.patch.object(host_statistics_instance, 'integrate')
+    mocked_times = mocker.patch.object(host_statistics_instance, '_push_times_stats')
+    mocked_cpu = mocker.patch.object(host_statistics_instance, '_push_cpu_stats')
+    mocked_mem = mocker.patch.object(host_statistics_instance, '_push_mem_stats')
+    mocked_io = mocker.patch.object(host_statistics_instance, '_push_io_stats')
+    # push first set of measures to become the first reference statistics
+    stats1 = {'now': 1.0, 'cpu': [[], [], []], 'io': {'lo': [], 'eth0': []}}
+    mocked_stats.return_value = 1.0, ['cpu_stats 1'], 76.1, {'io_stats 1': ()}
+    result = host_statistics_instance.push_statistics(stats1)
+    assert result == {}
     # check evolution of instance
-    assert statistics_instance.counter == 0
-    assert statistics_instance.ref_stats is stats1
+    assert host_statistics_instance.ref_start_time == 1.0
+    assert host_statistics_instance.ref_stats is stats1
     assert not mocked_stats.called
+    assert not mocked_times.called
     assert not mocked_cpu.called
     assert not mocked_mem.called
     assert not mocked_io.called
-    assert not mocked_proc.called
     # push second set of measures
-    stats2 = 'stats 2'
-    mocked_stats.return_value = (18.52, 'cpu_stats 2', 76.2, 'io_stats 2', 'proc_stats 2')
-    statistics_instance.push_statistics(stats2)
+    stats2 = {'now': 5.1, 'cpu': [[], [], []], 'io': {'lo': [], 'eth0': []}}
+    mocked_stats.return_value = 5.1, ['cpu_stats 2'], 76.2, {'io_stats 2': ()}
+    result = host_statistics_instance.push_statistics(stats2)
     # counter is based a theoretical period of 5 seconds so this update is not taken into account
+    assert result == {}
     # check evolution of instance
-    assert statistics_instance.counter == 1
-    assert statistics_instance.ref_stats is stats1
+    assert host_statistics_instance.ref_start_time == 1.0
+    assert host_statistics_instance.ref_stats is stats1
     assert not mocked_stats.called
+    assert not mocked_times.called
     assert not mocked_cpu.called
     assert not mocked_mem.called
     assert not mocked_io.called
-    assert not mocked_proc.called
     # push third set of measures
-    stats3 = 'stats 3'
-    mocked_stats.return_value = (28.5, 'cpu_stats 3', 76.1, 'io_stats 3', 'proc_stats 3')
-    statistics_instance.push_statistics(stats3)
+    stats3 = {'now': 13.2, 'cpu': [[], [], []], 'io': {'lo': [], 'eth0': []}}
+    mocked_stats.return_value = 12.2, ['cpu_stats 3'], 76.1, {'io_stats 3': ()}
+    result = host_statistics_instance.push_statistics(stats3)
     # this update is taken into account
+    assert result == {'identifier': '10.0.0.1',
+                      'target_period': 12,
+                      'period': (0.0, 12.2),
+                      'cpu': ['cpu_stats 3'],
+                      'mem': 76.1,
+                      'io': {'io_stats 3': ()}}
     # check evolution of instance
-    assert statistics_instance.counter == 2
-    assert statistics_instance.ref_stats is stats3
-    assert mocked_stats.call_args_list == [call(stats3, stats1)]
-    assert mocked_cpu.call_args_list == [call('cpu_stats 3')]
+    assert host_statistics_instance.ref_start_time == 1.0
+    assert host_statistics_instance.ref_stats is stats3
+    assert mocked_stats.call_args_list == [call(stats3)]
+    assert mocked_times.call_args_list == [call(12.2)]
+    assert mocked_cpu.call_args_list == [call(['cpu_stats 3'])]
     assert mocked_mem.call_args_list == [call(76.1)]
-    assert mocked_io.call_args_list == [call('io_stats 3')]
-    assert mocked_proc.call_args_list == [call('proc_stats 3')]
+    assert mocked_io.call_args_list == [call({'io_stats 3': ()})]
     mocker.resetall()
     # push fourth set of measures (reuse stats2)
-    statistics_instance.push_statistics(stats2)
-    # again,this update is not taken into account
-    assert statistics_instance.counter == 3
-    assert statistics_instance.ref_stats is stats3
+    result = host_statistics_instance.push_statistics(stats2)
+    # again, this update is not taken into account
+    assert result == {}
+    assert host_statistics_instance.ref_start_time == 1.0
+    assert host_statistics_instance.ref_stats is stats3
     assert not mocked_stats.called
+    assert not mocked_times.called
     assert not mocked_cpu.called
     assert not mocked_mem.called
     assert not mocked_io.called
-    assert not mocked_proc.called
     # push fifth set of measures
-    stats5 = 'stats 5'
-    mocked_stats.return_value = (38.5, 'cpu_stats 5', 75.9, 'io_stats 5', 'proc_stats 5')
-    statistics_instance.push_statistics(stats5)
+    stats5 = {'now': 27.4, 'cpu': [[], [], []], 'io': {'lo': [], 'eth0': []}}
+    mocked_stats.return_value = 38.5, ['cpu_stats 5'], 75.9, {'io_stats 5': ()}
+    result = host_statistics_instance.push_statistics(stats5)
     # this update is taken into account
+    assert result == {'identifier': '10.0.0.1',
+                      'target_period': 12,
+                      'period': (12.2, 26.4),
+                      'cpu': ['cpu_stats 5'],
+                      'mem': 75.9,
+                      'io': {'io_stats 5': ()}}
     # check evolution of instance
-    assert statistics_instance.counter == 4
-    assert statistics_instance.ref_stats is stats5
-    assert mocked_stats.call_args_list == [call(stats5, stats3)]
-    assert mocked_cpu.call_args_list == [call('cpu_stats 5')]
+    assert host_statistics_instance.ref_start_time == 1.0
+    assert host_statistics_instance.ref_stats is stats5
+    assert mocked_stats.call_args_list == [call(stats5)]
+    assert mocked_times.call_args_list == [call(38.5)]
+    assert mocked_cpu.call_args_list == [call(['cpu_stats 5'])]
     assert mocked_mem.call_args_list == [call(75.9)]
-    assert mocked_io.call_args_list == [call('io_stats 5')]
-    assert mocked_proc.call_args_list == [call('proc_stats 5')]
+    assert mocked_io.call_args_list == [call({'io_stats 5': ()})]
 
 
 @pytest.fixture
-def compiler(supvisors):
-    return StatisticsCompiler(supvisors)
-
-
-def test_compiler_create(supvisors, compiler):
-    """ Test the initialization for statistics of all addresses. """
-    # check compiler contents at initialisation
-    assert list(compiler.data.keys()) == list(supvisors.supvisors_mapper.instances.keys())
-    for period_instance in compiler.data.values():
-        assert list(period_instance.keys()) == supvisors.options.stats_periods
-        for period, instance in period_instance.items():
-            assert type(instance) is StatisticsInstance
-            assert instance.period == period / 5
-            assert instance.depth == supvisors.options.stats_histo
-
-
-def test_compiler_clear(compiler):
-    """ Test the clearance for statistics of all addresses. """
-    # set data to a given address
-    for address, period_instance in compiler.data.items():
-        for period, instance in period_instance.items():
-            instance.counter = 28
-            instance.ref_stats = ('dummy', 0)
-            instance.cpu = [13.2, 14.8]
-            instance.mem = [56.4, 71.3, 68.9]
-            instance.io = {'eth0': (123465, 654321), 'lo': (321, 321)}
-            instance.proc = {('myself', 5888): (25.0, 12.5)}
-    # check clearance of instance
-    compiler.clear('10.0.0.2')
-    for address, period_instance in compiler.data.items():
-        if address == '10.0.0.2':
-            for period, instance in period_instance.items():
-                assert instance.period == period / 5
-                assert instance.depth == 10
-                assert instance.counter == -1
-                assert instance.ref_stats is None
-                assert type(instance.cpu) is list
-                assert not instance.cpu
-                assert type(instance.mem) is list
-                assert not instance.mem
-                assert type(instance.io) is dict
-                assert not instance.io
-                assert type(instance.proc) is dict
-                assert not instance.proc
+def host_statistics_compiler(supvisors):
+    return HostStatisticsCompiler(supvisors)
+
+
+def test_host_statistics_compiler_creation(supvisors, host_statistics_compiler):
+    """ Test the creation of HostStatisticsCompiler. """
+    assert host_statistics_compiler.nb_cores == {}
+    identifiers = ['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4', '10.0.0.5',
+                   supvisors.supvisors_mapper.local_identifier, 'test']
+    assert sorted(host_statistics_compiler.instance_map.keys()) == identifiers
+    for period_map in host_statistics_compiler.instance_map.values():
+        assert sorted(period_map.keys()) == [5.0, 15.0, 60.0]
+        for period, period_instances in period_map.items():
+            assert isinstance(period_instances, HostStatisticsInstance)
+            assert period_instances.period == period
+            assert period_instances.depth == supvisors.options.stats_histo
+            assert period_instances.logger is supvisors.logger
+
+
+def test_host_statistics_compiler_get_stats(host_statistics_compiler):
+    """ Test the HostStatisticsCompiler.get_stats method """
+    # test with unknown identifier
+    assert host_statistics_compiler.get_stats('10.0.0.0', 5.0) is None
+    # test with correct identifier but unknown period
+    assert host_statistics_compiler.get_stats('10.0.0.1', 1.0) is None
+    # test with correct identifier and period
+    instance = host_statistics_compiler.get_stats('10.0.0.1', 15.0)
+    assert instance and instance.period == 15.0
+
+
+def test_host_statistics_compiler_get_nb_cores(host_statistics_compiler):
+    """ Test the HostStatisticsCompiler.get_nb_cores method """
+    # fill some internal structures
+    host_statistics_compiler.nb_cores = {'10.0.0.1': 4}
+    # test with unknown identifier
+    assert host_statistics_compiler.get_nb_cores('10.0.0.0') == 0
+    # test with known identifier but data not received yet
+    assert host_statistics_compiler.get_nb_cores('10.0.0.2') == 0
+    # test with known identifier and data available
+    assert host_statistics_compiler.get_nb_cores('10.0.0.1') == 4
+
+
+def test_host_statistics_compiler_push_statistics(mocker, host_statistics_compiler):
+    """ Test the HostStatisticsCompiler.push_statistics method """
+    for identifier, period_maps in host_statistics_compiler.instance_map.items():
+        for instance in period_maps.values():
+            mocker.patch.object(instance, 'push_statistics')
+    # test with unknown identifier
+    host_stats = {'cpu': [1, 2, 3, 4, 5]}
+    host_statistics_compiler.push_statistics('10.0.0.0', host_stats)
+    assert host_statistics_compiler.nb_cores == {}
+    assert all((not instance.push_statistics.called
+                for instance in period_maps.values()))
+    # test with known identifier
+    host_statistics_compiler.push_statistics('10.0.0.1', host_stats)
+    assert host_statistics_compiler.nb_cores == {'10.0.0.1': 4}
+    for identifier, period_maps in host_statistics_compiler.instance_map.items():
+        if identifier == '10.0.0.1':
+            assert all((instance.push_statistics.call_args_list == [call(host_stats)]
+                        for instance in period_maps.values()))
         else:
-            for period, instance in period_instance.items():
-                assert instance.period == period / 5
-                assert instance.depth == 10
-                assert instance.counter == 28
-                assert instance.ref_stats == ('dummy', 0)
-                assert instance.cpu == [13.2, 14.8]
-                assert instance.mem == [56.4, 71.3, 68.9]
-                assert instance.io == {'eth0': (123465, 654321), 'lo': (321, 321)}
-                assert instance.proc == {('myself', 5888): (25.0, 12.5)}
-
-
-def test_compiler_push_statistics(compiler):
-    """ Test the storage of the instant statistics of an address. """
-    # push statistics to a given address
-    stats1 = (8.5, [(25, 400), (25, 125), (15, 150), (40, 400), (20, 200)],
-              76.1, {'eth0': (1024, 2000), 'lo': (500, 500)}, {'myself': (118612, (0.15, 1.85))})
-    compiler.push_statistics('10.0.0.2', stats1)
-    # check compiler contents
-    for address, period_instance in compiler.data.items():
-        if address == '10.0.0.2':
-            for period, instance in period_instance.items():
-                assert instance.counter == 0
-                assert instance.ref_stats is stats1
-        else:
-            for period, instance in period_instance.items():
-                assert instance.counter == -1
-                assert instance.ref_stats is None
-    # push statistics to a given address
-    stats2 = (28.5, [(45, 700), (50, 225), (40, 250), (42, 598), (20, 400)],
-              76.1, {'eth0': (2048, 2512), 'lo': (756, 756)}, {'myself': (118612, (1.75, 1.9))})
-    compiler.push_statistics('10.0.0.2', stats2)
-    # check compiler contents
-    for address, period_instance in compiler.data.items():
-        if address == '10.0.0.2':
-            for period, instance in period_instance.items():
-                assert instance.counter == 1
-                if period == 5:
-                    assert instance.ref_stats is stats2
-                else:
-                    assert instance.ref_stats is stats1
-        else:
-            for period, instance in period_instance.items():
-                assert instance.counter == -1
-                assert instance.ref_stats is None
-    # push statistics to a given address
-    stats3 = (38.5, [(80, 985), (89, 386), (48, 292), (42, 635), (32, 468)],
-              75.9, {'eth0': (3072, 2768), 'lo': (1780, 1780)}, {'myself': (118612, (11.75, 1.87))})
-    compiler.push_statistics('10.0.0.2', stats3)
-    # check compiler contents
-    for address, period_instance in compiler.data.items():
-        if address == '10.0.0.2':
-            for period, instance in period_instance.items():
-                assert instance.counter == 2
-                if period == 5:
-                    assert instance.ref_stats is stats3
-                else:
-                    assert instance.ref_stats is stats1
-        else:
-            for period, instance in period_instance.items():
-                assert instance.counter == -1
-                assert instance.ref_stats is None
-    # push statistics to a given address
-    stats4 = (48.5, [(84, 1061), (92, 413), (48, 480), (45, 832), (40, 1100)],
-              74.7, {'eth0': (3584, 3792), 'lo': (1812, 1812)}, {'myself': (118612, (40.75, 2.34))})
-    compiler.push_statistics('10.0.0.2', stats4)
-    # check compiler contents
-    for address, period_instance in compiler.data.items():
-        if address == '10.0.0.2':
-            for period, instance in period_instance.items():
-                assert instance.counter == 3
-                if period in [5, 15]:
-                    assert instance.ref_stats is stats4
-                else:
-                    assert instance.ref_stats is stats1
-        else:
-            for period, instance in period_instance.items():
-                assert instance.counter == -1
-                assert instance.ref_stats is None
+            assert all((not instance.push_statistics.called
+                        for instance in period_maps.values()))
+
+
+# Process Statistics part
+def test_cpu_process_statistics():
+    """ Test the CPU of the process between 2 dates. """
+    stats = cpu_process_statistics(50, 20, 100)
+    assert type(stats) is float
+    assert pytest.approx(stats) == 30
+
+
+@pytest.fixture
+def proc_statistics_instance():
+    # testing with period 12 and history depth 2
+    return ProcStatisticsInstance('dummy_proc', '10.0.0.1', 12, 2)
+
+
+def test_proc_statistics_instance_creation(proc_statistics_instance):
+    """ Test the creation of ProcStatisticsInstance. """
+    assert proc_statistics_instance.namespec == 'dummy_proc'
+    assert proc_statistics_instance.identifier == '10.0.0.1'
+    assert proc_statistics_instance.period == 12
+    assert proc_statistics_instance.depth == 2
+    assert proc_statistics_instance.ref_stats == {}
+    assert proc_statistics_instance.ref_start_time == 0.0
+    assert proc_statistics_instance.times == []
+    assert proc_statistics_instance.cpu == []
+    assert proc_statistics_instance.mem == []
+
+
+def test_proc_statistics_instance_integrate(proc_statistics_instance):
+    """ Test the ProcStatisticsInstance.integrate method. """
+    proc_statistics_instance.ref_start_time = 800
+    proc_statistics_instance.ref_stats = {'now': 1000, 'cpu': (25, 400),
+                                          'proc_work': 0.15,
+                                          'proc_memory': 1.85}
+    last_stats = {'now': 1002, 'cpu': (45, 700),
+                  'proc_work': 1.75,
+                      'proc_memory': 1.9}
+    proc_cpu, proc_mem, duration = proc_statistics_instance.integrate(last_stats)
+    # check process stats
+    assert proc_cpu == 0.5
+    assert proc_mem == 1.9
+    assert duration == 202
+
+
+def test_proc_statistics_instance_push_statistics(mocker, proc_statistics_instance):
+    """ Test the ProcStatisticsInstance.push_statistics method. """
+    mocked_integ = mocker.patch.object(proc_statistics_instance, 'integrate')
+    # push first set of measures to become the first reference statistics
+    stats1 = {'now': 1.0}
+    mocked_integ.return_value = (5.2, 26.1, 1.0)
+    # first push used as reference only
+    result = proc_statistics_instance.push_statistics(stats1)
+    assert result == {}
+    assert not mocked_integ.called
+    assert proc_statistics_instance.cpu == []
+    assert proc_statistics_instance.mem == []
+    assert proc_statistics_instance.times == []
+    assert proc_statistics_instance.ref_stats == stats1
+    assert proc_statistics_instance.ref_start_time == 1.0
+    # from second push, integration is performed when period has passed
+    # test when it didn't pass
+    stats2 = {'now': 7.0}
+    result = proc_statistics_instance.push_statistics(stats2)
+    assert result == {}
+    assert not mocked_integ.called
+    assert proc_statistics_instance.cpu == []
+    assert proc_statistics_instance.mem == []
+    assert proc_statistics_instance.times == []
+    assert proc_statistics_instance.ref_stats == stats1
+    assert proc_statistics_instance.ref_start_time == 1.0
+    # test when it has passed
+    stats3 = {'now': 15.0}
+    result = proc_statistics_instance.push_statistics(stats3)
+    assert result == {'namespec': 'dummy_proc',
+                      'identifier': '10.0.0.1',
+                      'target_period': 12,
+                      'period': (0.0, 14.0),
+                      'cpu': 5.2,
+                      'mem': 26.1}
+    assert mocked_integ.call_args_list == [call(stats3)]
+    assert proc_statistics_instance.cpu == [5.2]
+    assert proc_statistics_instance.mem == [26.1]
+    assert proc_statistics_instance.times == [1.0]
+    assert proc_statistics_instance.ref_stats == stats3
+    assert proc_statistics_instance.ref_start_time == 1.0
+    mocked_integ.reset_mock()
+    # test second push
+    stats4 = {'now': 30.0}
+    mocked_integ.return_value = (6.9, 13.7, 15.0)
+    result = proc_statistics_instance.push_statistics(stats4)
+    assert result == {'namespec': 'dummy_proc',
+                      'identifier': '10.0.0.1',
+                      'target_period': 12,
+                      'period': (14.0, 29.0),
+                      'cpu': 6.9,
+                      'mem': 13.7}
+    assert mocked_integ.call_args_list == [call(stats4)]
+    assert proc_statistics_instance.cpu == [5.2, 6.9]
+    assert proc_statistics_instance.mem == [26.1, 13.7]
+    assert proc_statistics_instance.times == [1.0, 15.0]
+    assert proc_statistics_instance.ref_stats == stats4
+    assert proc_statistics_instance.ref_start_time == 1.0
+    mocked_integ.reset_mock()
+    # test third push for history rotation
+    stats5 = {'now': 45.0}
+    mocked_integ.return_value = (4.4, 8.9, 15.0)
+    result = proc_statistics_instance.push_statistics(stats5)
+    assert result == {'namespec': 'dummy_proc',
+                      'identifier': '10.0.0.1',
+                      'target_period': 12,
+                      'period': (29.0, 44.0),
+                      'cpu': 4.4,
+                      'mem': 8.9}
+    assert mocked_integ.call_args_list == [call(stats5)]
+    assert proc_statistics_instance.cpu == [6.9, 4.4]
+    assert proc_statistics_instance.mem == [13.7, 8.9]
+    assert proc_statistics_instance.times == [15.0, 15.0]
+    assert proc_statistics_instance.ref_stats == stats5
+    assert proc_statistics_instance.ref_start_time == 1.0
+
+
+@pytest.fixture
+def proc_statistics_holder(supvisors):
+    return ProcStatisticsHolder('dummy_proc', supvisors.options, supvisors.logger)
+
+
+def test_proc_statistics_holder_creation(supvisors, proc_statistics_holder):
+    """ Test the creation of ProcStatisticsHolder. """
+    assert proc_statistics_holder.namespec == 'dummy_proc'
+    assert proc_statistics_holder.options is supvisors.options
+    assert proc_statistics_holder.logger is supvisors.logger
+    assert proc_statistics_holder.instance_map == {}
+
+
+def test_proc_statistics_holder_get_stats(proc_statistics_holder):
+    """ Test the search method for process statistics. """
+    # change values
+    proc_statistics_holder.instance_map = {'10.0.0.1': (os.getpid(), {5: 'proc stats 5s on 10.0.0.1'})}
+    # test find method with wrong identifier
+    assert proc_statistics_holder.get_stats('10.0.0.2', 5) is None
+    # test find method with correct identifier and wrong period
+    assert proc_statistics_holder.get_stats('10.0.0.1', 10) is None
+    # test find method with correct identifier and period
+    assert proc_statistics_holder.get_stats('10.0.0.1', 5) == 'proc stats 5s on 10.0.0.1'
+
+
+def test_proc_statistics_holder_push_statistics(mocker, proc_statistics_holder):
+    """ Test the storage of the process instant statistics. """
+    mocked_instance = mocker.patch('supvisors.statscompiler.ProcStatisticsInstance',
+                                   side_effect=lambda a, b, c, d: Mock(time_label=time.time(),
+                                                                       namespec=a, identifier=b, period=c, depth=d,
+                                                                       **{'push_statistics.return_value': None}))
+    # 1. test with running process not referenced yet
+    proc_stats = {'pid': 118612}
+    result = proc_statistics_holder.push_statistics('10.0.0.1', proc_stats)
+    assert result == []
+    assert list(proc_statistics_holder.instance_map.keys()) == ['10.0.0.1']
+    # check host stats created
+    pid, identifier_instances = proc_statistics_holder.instance_map['10.0.0.1']
+    assert pid == 118612
+    assert sorted(identifier_instances.keys()) == [5.0, 15.0, 60.0]
+    for period, instance in identifier_instances.items():
+        assert instance.namespec == 'dummy_proc'
+        assert instance.identifier == '10.0.0.1'
+        assert instance.period == period
+        assert instance.depth == 10
+        assert instance.push_statistics.call_args_list == [call(proc_stats)]
+        instance.push_statistics.reset_mock()
+    # 2. test with running process not referenced yet on another host
+    proc_stats = {'pid': 612}
+    result = proc_statistics_holder.push_statistics('10.0.0.2', proc_stats)
+    assert result == []
+    assert sorted(proc_statistics_holder.instance_map.keys()) == ['10.0.0.1', '10.0.0.2']
+    # check no change on first host stats
+    pid, identifier_instances = proc_statistics_holder.instance_map['10.0.0.1']
+    assert pid == 118612
+    assert sorted(identifier_instances.keys()) == [5.0, 15.0, 60.0]
+    for period, instance in identifier_instances.items():
+        assert instance.namespec == 'dummy_proc'
+        assert instance.identifier == '10.0.0.1'
+        assert instance.period == period
+        assert instance.depth == 10
+        assert not instance.push_statistics.called
+    # check second host stats created
+    pid, identifier_instances = proc_statistics_holder.instance_map['10.0.0.2']
+    assert pid == 612
+    assert sorted(identifier_instances.keys()) == [5.0, 15.0, 60.0]
+    for period, instance in identifier_instances.items():
+        assert instance.namespec == 'dummy_proc'
+        assert instance.identifier == '10.0.0.2'
+        assert instance.period == period
+        assert instance.depth == 10
+        assert instance.push_statistics.call_args_list == [call(proc_stats)]
+        instance.push_statistics.reset_mock()
+    # 3. test with running process with a different pid on existing host
+    mocked_instance.side_effect=lambda a, b, c, d: Mock(time_label=time.time(),
+                                                        namespec=a, identifier=b, period=c, depth=d,
+                                                        **{'push_statistics.return_value': f'{b}_{c}'})
+    proc_stats = {'pid': 18612}
+    result = proc_statistics_holder.push_statistics('10.0.0.1', proc_stats)
+    assert result == ['10.0.0.1_5.0', '10.0.0.1_15.0', '10.0.0.1_60.0']
+    assert sorted(proc_statistics_holder.instance_map.keys()) == ['10.0.0.1', '10.0.0.2']
+    # check host instances are replaced
+    pid, identifier_instances = proc_statistics_holder.instance_map['10.0.0.1']
+    assert pid == 18612
+    assert sorted(identifier_instances.keys()) == [5.0, 15.0, 60.0]
+    for period, instance in identifier_instances.items():
+        assert instance.namespec == 'dummy_proc'
+        assert instance.identifier == '10.0.0.1'
+        assert instance.period == period
+        assert instance.depth == 10
+        assert instance.push_statistics.call_args_list == [call(proc_stats)]
+        instance.push_statistics.reset_mock()
+    # test no change on second host stats
+    pid, identifier_instances = proc_statistics_holder.instance_map['10.0.0.2']
+    assert pid == 612
+    assert sorted(identifier_instances.keys()) == [5.0, 15.0, 60.0]
+    for period, instance in identifier_instances.items():
+        assert instance.namespec == 'dummy_proc'
+        assert instance.identifier == '10.0.0.2'
+        assert instance.period == period
+        assert instance.depth == 10
+        assert not instance.push_statistics.called
+    # 4. test with stopped process. instances are deleted
+    proc_stats = {'pid': 0}
+    result = proc_statistics_holder.push_statistics('10.0.0.1', proc_stats)
+    assert result == []
+    assert list(proc_statistics_holder.instance_map.keys()) == ['10.0.0.2']
+    # test no change on second host stats
+    pid, identifier_instances = proc_statistics_holder.instance_map['10.0.0.2']
+    assert pid == 612
+    assert sorted(identifier_instances.keys()) == [5.0, 15.0, 60.0]
+    for period, instance in identifier_instances.items():
+        assert instance.namespec == 'dummy_proc'
+        assert instance.identifier == '10.0.0.2'
+        assert instance.period == period
+        assert instance.depth == 10
+        assert not instance.push_statistics.called
+
+
+@pytest.fixture
+def proc_statistics_compiler(supvisors):
+    return ProcStatisticsCompiler(supvisors.options, supvisors.logger)
+
+
+def test_proc_statistics_compiler_creation(supvisors, proc_statistics_compiler):
+    """ Test the creation of ProcStatisticsCompiler. """
+    assert proc_statistics_compiler.options is supvisors.options
+    assert proc_statistics_compiler.logger is supvisors.logger
+    assert proc_statistics_compiler.holder_map == {}
+    assert proc_statistics_compiler.nb_cores == {}
+
+
+
+def test_proc_statistics_compiler_get_stats(mocker, proc_statistics_compiler):
+    """ Test the ProcStatisticsCompiler.get_stats method """
+    mocked_stats = mocker.patch('supvisors.statscompiler.ProcStatisticsHolder.get_stats')
+    # test on unknown namespec
+    assert proc_statistics_compiler.get_stats('dummy_proc', '10.0.0.1', '12.5') is None
+    assert not mocked_stats.called
+    # fill some data
+    mocked_holder = Mock(**{'get_stats.return_value': 'some stats'})
+    proc_statistics_compiler.holder_map['dummy_proc'] = mocked_holder
+    # test on known namespec
+    assert proc_statistics_compiler.get_stats('dummy_proc', '10.0.0.1', '12.5') == 'some stats'
+    assert mocked_holder.get_stats.call_args_list == [call('10.0.0.1', '12.5')]
+
+
+def test_proc_statistics_compiler_get_nb_cores(proc_statistics_compiler):
+    """ Test the ProcStatisticsCompiler.get_nb_cores method """
+    # test on unknown identifier
+    assert proc_statistics_compiler.get_nb_cores('10.0.0.1') == 0
+    # fill some data
+    proc_statistics_compiler.nb_cores['10.0.0.1'] = 4
+    # test on known namespec
+    assert proc_statistics_compiler.get_nb_cores('10.0.0.1') == 4
+
+
+def test_proc_statistics_compiler_push_statistics(mocker, proc_statistics_compiler):
+    """ Test the ProcStatisticsCompiler.push_statistics method """
+    mocked_holder = Mock(instance_map=True, **{'push_statistics.return_value': ['dummy_proc stats']})
+    mocked_holder_constr = mocker.patch('supvisors.statscompiler.ProcStatisticsHolder', return_value=mocked_holder)
+    process_stats = {'namespec': 'dummy_proc', 'pid': 0}
+    # 1. test with non-existing holder and 0 pid
+    result = proc_statistics_compiler.push_statistics('10.0.0.1', [process_stats])
+    assert result == []
+    assert proc_statistics_compiler.nb_cores == {}
+    assert proc_statistics_compiler.holder_map == {}
+    assert not mocked_holder_constr.called
+    assert not mocked_holder.push_statistics.called
+    # 2. test with non-existing holder and pid > 0
+    process_stats['pid'] = 1234
+    result = proc_statistics_compiler.push_statistics('10.0.0.1', [process_stats])
+    assert result == ['dummy_proc stats']
+    assert proc_statistics_compiler.nb_cores == {}
+    assert list(proc_statistics_compiler.holder_map.keys()) == ['dummy_proc']
+    assert mocked_holder_constr.call_args_list == [call('dummy_proc', proc_statistics_compiler.options,
+                                                        proc_statistics_compiler.logger)]
+    assert mocked_holder.push_statistics.call_args_list == [call('10.0.0.1', process_stats)]
+    mocked_holder_constr.reset_mock()
+    mocked_holder.push_statistics.reset_mock()
+    # 3. test with existing holder and pid > 0
+    process_stats['nb_cores'] = 4
+    result = proc_statistics_compiler.push_statistics('10.0.0.1', [process_stats])
+    assert result == ['dummy_proc stats']
+    assert proc_statistics_compiler.nb_cores == {'10.0.0.1': 4}
+    assert list(proc_statistics_compiler.holder_map.keys()) == ['dummy_proc']
+    assert not mocked_holder_constr.called
+    assert mocked_holder.push_statistics.call_args_list == [call('10.0.0.1', process_stats)]
+    mocked_holder.push_statistics.reset_mock()
+    # 4. test with existing holder and 0 pid
+    process_stats['pid'] = 0
+    process_stats['nb_cores'] = 6
+    result = proc_statistics_compiler.push_statistics('10.0.0.1', [process_stats])
+    assert result == ['dummy_proc stats']
+    assert proc_statistics_compiler.nb_cores == {'10.0.0.1': 6}
+    assert list(proc_statistics_compiler.holder_map.keys()) == ['dummy_proc']
+    assert not mocked_holder_constr.called
+    assert mocked_holder.push_statistics.call_args_list == [call('10.0.0.1', process_stats)]
+    mocked_holder.push_statistics.reset_mock()
+    # test case when instance_map has been removed
+    mocked_holder.instance_map = {}
+    result = proc_statistics_compiler.push_statistics('10.0.0.1', [process_stats])
+    assert result == ['dummy_proc stats']
+    assert proc_statistics_compiler.nb_cores == {'10.0.0.1': 6}
+    assert proc_statistics_compiler.holder_map == {}
+    assert not mocked_holder_constr.called
+    assert mocked_holder.push_statistics.call_args_list == [call('10.0.0.1', process_stats)]
diff --git a/supvisors/tests/test_strategy.py b/supvisors/tests/test_strategy.py
index a9f683b8..c2419ab0 100644
--- a/supvisors/tests/test_strategy.py
+++ b/supvisors/tests/test_strategy.py
@@ -17,10 +17,10 @@
 # limitations under the License.
 # ======================================================================
 
-import pytest
-
 from unittest.mock import Mock, call
 
+import pytest
+
 from supvisors.instancestatus import SupvisorsInstanceStatus
 from supvisors.strategy import *
 from supvisors.ttypes import (SupvisorsInstanceStates, ConciliationStrategies, RunningFailureStrategies,
@@ -50,7 +50,7 @@ def filled_instances(mocker, supvisors):
 @pytest.fixture
 def load_details(supvisors):
     local_identifier = supvisors.supvisors_mapper.local_identifier
-    local_node_name = supvisors.supvisors_mapper.instances[local_identifier].host_name
+    local_node_name = supvisors.supvisors_mapper.instances[local_identifier].host_id
     return ({local_identifier: 0, '10.0.0.3': 10, '10.0.0.5': 20, 'test': 10},
             {local_node_name: 50, '10.0.0.3': 20, '10.0.0.5': 80},
             {local_node_name: 10, '10.0.0.3': 10, '10.0.0.5': 20})
@@ -257,7 +257,7 @@ def test_get_node(mocker, filled_instances):
     mocked_get_instance.return_value = 'test'
     strategy = StartingStrategies.CONFIG
     local_instance = filled_instances.supvisors_mapper.instances[local_identifier]
-    assert get_node(filled_instances, strategy, instances, 27) == local_instance.host_name
+    assert get_node(filled_instances, strategy, instances, 27) == local_instance.host_id
 
 
 def create_process_status(name, timed_identifiers):
diff --git a/supvisors/tests/test_supervisordata.py b/supvisors/tests/test_supervisordata.py
index 4546923a..70ce2595 100644
--- a/supvisors/tests/test_supervisordata.py
+++ b/supvisors/tests/test_supervisordata.py
@@ -68,11 +68,12 @@ def test_creation(supervisor, source):
 def test_accessors(source):
     """ Test the accessors. """
     # test consistence with DummySupervisor configuration
-    assert source.httpserver is source.supervisord.options.httpserver
+    assert source.http_server is source.supervisord.options.httpserver
     assert source.supervisor_rpc_interface.rpc_name == 'supervisor_RPC'
     assert source.supvisors_rpc_interface.rpc_name == 'supvisors_RPC'
-    assert source.serverurl == f'http://{gethostname()}:65000'
-    assert source.serverport == 65000
+    assert source.server_host == gethostname()
+    assert source.server_port == 65000
+    assert source.server_url == f'http://{gethostname()}:65000'
     assert source.username == 'user'
     assert source.password == 'p@$$w0rd'
     assert source.supervisor_state == SupervisorStates.RUNNING
diff --git a/supvisors/tests/test_supvisorsctl.py b/supvisors/tests/test_supvisorsctl.py
index 0d5b90dd..adca25b3 100644
--- a/supvisors/tests/test_supvisorsctl.py
+++ b/supvisors/tests/test_supvisorsctl.py
@@ -321,9 +321,11 @@ def test_instance_status(controller, plugin, mocked_check):
     mocked_rpc = plugin.supvisors().get_all_instances_info
     mocked_rpc.return_value = [{'identifier': '10.0.0.1', 'node_name': '10.0.0.1', 'port': 60000,
                                 'statename': 'running', 'loading': 10, 'local_time': 1500, 'sequence_counter': 12,
+                                'process_failure': False,
                                 'fsm_statename': 'OPERATION', 'starting_jobs': True, 'stopping_jobs': False},
                                {'identifier': '10.0.0.2', 'node_name': '10.0.0.2', 'port': 60000,
                                 'statename': 'stopped', 'loading': 0, 'local_time': 100, 'sequence_counter': 15,
+                                'process_failure': True,
                                 'fsm_statename': 'CONCILATION', 'starting_jobs': False, 'stopping_jobs': True}]
     _check_call(controller, mocked_check, mocked_rpc,  plugin.help_instance_status, plugin.do_instance_status,
                 '', [call()])
@@ -551,7 +553,7 @@ def test_all_start(mocker, controller, plugin, mocked_check):
 
 
 def test_all_start_args(mocker, controller, plugin, mocked_check):
-    """ TODO: Test the all_start_args request. """
+    """ Test the all_start_args request. """
     mocked_get_instances = mocker.patch.object(plugin, 'get_running_instances')
     mocked_rpc = Mock(**{'start_args.return_value': True})
     mocked_get_proxy = mocker.patch.object(plugin, 'get_server_proxy', return_value=mocked_rpc)
diff --git a/supvisors/tests/test_supvisorsmapper.py b/supvisors/tests/test_supvisorsmapper.py
index 4b5823d5..2e708431 100644
--- a/supvisors/tests/test_supvisorsmapper.py
+++ b/supvisors/tests/test_supvisorsmapper.py
@@ -17,118 +17,124 @@
 # limitations under the License.
 # ======================================================================
 
+from socket import gethostname
+
 import pytest
 
 from supvisors.supvisorsmapper import *
 
 
-def test_get_node_names(supvisors):
-    """ Test the SupvisorsMapper.get_node_names method. """
+def test_get_addresses(supvisors):
+    """ Test the SupvisorsMapper.get_addresses method. """
     # complex to test as it depends on the network configuration of the operating system
     # check that there is at least one entry looking like an IPv4 address
-    ip_list = get_node_names(gethostname(), supvisors.logger)
+    host_name, aliases, ip_list = get_addresses(gethostname(), supvisors.logger)
     assert re.match(r'^\d{1,3}(.\d{1,3}){3}$', ip_list[0])
+    assert host_name == getfqdn()
     # test exception on unknown IP address and unknown host name
-    assert get_node_names('10.4', supvisors.logger) == []
-    assert get_node_names('unknown node', supvisors.logger) == []
+    assert get_addresses('10.4', supvisors.logger) is None
+    assert get_addresses('unknown node', supvisors.logger) is None
 
 
-def test_supid_create_no_match(supvisors):
+def test_sup_id_create_no_match(supvisors):
     """ Test the values set at SupvisorsInstanceId construction. """
     no_matches = ['', 'ident>', 'cliche81:12000', '10.0.0.1:145000:28']
     # test with no match but default options loaded
     for item in no_matches:
-        supid = SupvisorsInstanceId(item, supvisors)
-        assert supid.identifier is None
-        assert supid.host_name is None
-        assert supid.ip_address is None
-        assert supid.http_port == 65000  # defaulted to supervisor server port
-        assert supid.internal_port == 65100  # defaulted to options.internal_port
-        assert supid.event_port == 65200  # defaulted to options.event_port
+        sup_id = SupvisorsInstanceId(item, supvisors)
+        assert sup_id.identifier is None
+        assert sup_id.host_id is None
+        assert sup_id.http_port == 65000  # defaulted to supervisor server port
+        assert sup_id.internal_port == 65100  # defaulted to options.internal_port
+        assert sup_id.event_port == 65200  # defaulted to options.event_port
+        assert sup_id.host_name is None
+        assert sup_id.ip_address is None
         with pytest.raises(TypeError):
-            str(supid)
+            str(sup_id)
 
 
-def test_supid_create_simple_no_default(supvisors):
+def test_sup_id_create_simple_no_default(supvisors):
     """ Test the values set at SupvisorsInstanceId construction. """
     supvisors.options.internal_port = 0
     supvisors.options.event_port = 0
     # test with no match and no default option loaded
-    supid = SupvisorsInstanceId('', supvisors)
-    assert supid.identifier is None
-    assert supid.host_name is None
-    assert supid.ip_address is None
-    assert supid.http_port == 65000  # defaulted to supervisor server port
-    assert supid.internal_port == 65001  # defaulted to http_port + 1
-    assert supid.event_port == 65002  # defaulted to http_port + 2
+    sup_id = SupvisorsInstanceId('', supvisors)
+    assert sup_id.identifier is None
+    assert sup_id.host_id is None
+    assert sup_id.http_port == 65000  # defaulted to supervisor server port
+    assert sup_id.internal_port == 65001  # defaulted to http_port + 1
+    assert sup_id.event_port == 65002  # defaulted to http_port + 2
+    assert sup_id.host_name is None
+    assert sup_id.ip_address is None
     with pytest.raises(TypeError):
-        str(supid)
+        str(sup_id)
 
 
-def test_supid_create_host(supvisors):
+def test_sup_id_create_host(supvisors):
     """ Test the values set at SupvisorsInstanceId construction. """
     # test with simple host name match
-    supid = SupvisorsInstanceId('10.0.0.1', supvisors)
-    assert supid.identifier == '10.0.0.1'
-    assert supid.host_name == '10.0.0.1'
-    assert supid.ip_address == '10.0.0.1'
-    assert supid.http_port == 65000  # defaulted to supervisor server port
-    assert supid.internal_port == 65100  # defaulted to options.internal_port
-    assert supid.event_port == 65200  # defaulted to options.event_port
-    assert str(supid) == '10.0.0.1'
-
-
-def test_supid_create_host_port(supvisors):
+    sup_id = SupvisorsInstanceId('10.0.0.1', supvisors)
+    assert sup_id.identifier == '10.0.0.1'
+    assert sup_id.host_id == '10.0.0.1'
+    assert sup_id.http_port == 65000  # defaulted to supervisor server port
+    assert sup_id.internal_port == 65100  # defaulted to options.internal_port
+    assert sup_id.event_port == 65200  # defaulted to options.event_port
+    assert sup_id.host_name == '10.0.0.1'
+    assert sup_id.ip_address == '10.0.0.1'
+    assert str(sup_id) == '10.0.0.1'
+
+
+def test_sup_id_create_host_port(supvisors):
     """ Test the values set at SupvisorsInstanceId construction. """
     # test with host+ports match (internal port not defined but still in options)
-    supid = SupvisorsInstanceId('10.0.0.1:7777:', supvisors)
-    assert supid.identifier == '10.0.0.1:7777'
-    assert supid.host_name == '10.0.0.1'
-    assert supid.ip_address == '10.0.0.1'
-    assert supid.http_port == 7777
-    assert supid.internal_port == 65100  # defaulted to options.internal_port
-    assert supid.event_port == 65200  # defaulted to options.event_port
-    assert str(supid) == '10.0.0.1:7777'
+    sup_id = SupvisorsInstanceId('10.0.0.1:7777:', supvisors)
+    assert sup_id.identifier == '10.0.0.1:7777'
+    assert sup_id.host_id == '10.0.0.1'
+    assert sup_id.ip_address == '10.0.0.1'
+    assert sup_id.http_port == 7777
+    assert sup_id.internal_port == 65100  # defaulted to options.internal_port
+    assert sup_id.event_port == 65200  # defaulted to options.event_port
+    assert str(sup_id) == '10.0.0.1:7777'
     # test with host+ports match (internal port defined)
     supvisors.options.event_port = 0
-    supid = SupvisorsInstanceId('10.0.0.1:7777:8000', supvisors)
-    assert supid.identifier == '10.0.0.1:7777'
-    assert supid.host_name == '10.0.0.1'
-    assert supid.ip_address == '10.0.0.1'
-    assert supid.http_port == 7777
-    assert supid.internal_port == 8000
-    assert supid.event_port == 8001  # defaulted to internal_port + 1
-    assert str(supid) == '10.0.0.1:7777'
+    sup_id = SupvisorsInstanceId('10.0.0.1:7777:8000', supvisors)
+    assert sup_id.identifier == '10.0.0.1:7777'
+    assert sup_id.host_id == '10.0.0.1'
+    assert sup_id.ip_address == '10.0.0.1'
+    assert sup_id.http_port == 7777
+    assert sup_id.internal_port == 8000
+    assert sup_id.event_port == 8001  # defaulted to internal_port + 1
+    assert str(sup_id) == '10.0.0.1:7777'
     # test with host+ports match (internal port defined before HTTP port)
     supvisors.options.event_port = 0
-    supid = SupvisorsInstanceId('10.0.0.1:7777:7776', supvisors)
-    assert supid.identifier == '10.0.0.1:7777'
-    assert supid.host_name == '10.0.0.1'
-    assert supid.ip_address == '10.0.0.1'
-    assert supid.http_port == 7777
-    assert supid.internal_port == 7776
-    assert supid.event_port == 7778  # defaulted to http_port + 1
-    assert str(supid) == '10.0.0.1:7777'
+    sup_id = SupvisorsInstanceId('10.0.0.1:7777:7776', supvisors)
+    assert sup_id.identifier == '10.0.0.1:7777'
+    assert sup_id.host_id == '10.0.0.1'
+    assert sup_id.ip_address == '10.0.0.1'
+    assert sup_id.http_port == 7777
+    assert sup_id.internal_port == 7776
+    assert sup_id.event_port == 7778  # defaulted to http_port + 1
+    assert str(sup_id) == '10.0.0.1:7777'
 
 
-def test_supid_create_identifier(supvisors):
+def test_sup_id_create_identifier(supvisors):
     """ Test the values set at SupvisorsInstanceId construction. """
     # test with identifier set and only host name
-    supid = SupvisorsInstanceId('<supvisors>cliche81', supvisors)
-    assert supid.identifier == 'supvisors'
-    assert supid.host_name == 'cliche81'
-    assert supid.ip_address == 'cliche81'
-    assert supid.http_port == 65000
-    assert supid.internal_port == 65100  # defaulted to options.internal_port
-    assert supid.event_port == 65200  # defaulted to options.event_port
+    sup_id = SupvisorsInstanceId('<supvisors>cliche81', supvisors)
+    assert sup_id.identifier == 'supvisors'
+    assert sup_id.host_id == 'cliche81'
+    assert sup_id.ip_address == 'cliche81'
+    assert sup_id.http_port == 65000
+    assert sup_id.internal_port == 65100  # defaulted to options.internal_port
+    assert sup_id.event_port == 65200  # defaulted to options.event_port
     # test with identifier set
-    supid = SupvisorsInstanceId('<supvisors>cliche81:8888:5555', supvisors)
-    assert supid.identifier == 'supvisors'
-    assert supid.host_name == 'cliche81'
-    assert supid.ip_address == 'cliche81'
-    assert supid.http_port == 8888
-    assert supid.internal_port == 5555  # defaulted to options.internal_port
-    assert supid.event_port == 65200  # defaulted to options.event_port
+    sup_id = SupvisorsInstanceId('<supvisors>cliche81:8888:5555', supvisors)
+    assert sup_id.identifier == 'supvisors'
+    assert sup_id.host_id == 'cliche81'
+    assert sup_id.ip_address == 'cliche81'
+    assert sup_id.http_port == 8888
+    assert sup_id.internal_port == 5555  # defaulted to options.internal_port
+    assert sup_id.event_port == 65200  # defaulted to options.event_port
 
 
 @pytest.fixture
@@ -144,8 +150,6 @@ def test_mapper_create(supvisors, mapper):
     assert mapper.instances == {}
     assert mapper.nodes == {}
     assert mapper.local_identifier is None
-    # check that hostname is part of the local addresses
-    assert gethostname() in mapper.local_node_references
 
 
 def test_mapper_configure(mocker, mapper):
@@ -172,44 +176,84 @@ def test_find_local_identifier_identifier(mapper):
     """ Test the SupvisorsMapper.find_local_identifier method when Supervisor identifier is among the instances. """
     host_name = gethostname()
     items = [host_name, '<supervisor>10.0.0.5:7777:']
-    mapper.configure(items, [])
+    for item in items:
+        supvisors_id = SupvisorsInstanceId(item, mapper.supvisors)
+        mapper._instances[supvisors_id.identifier] = supvisors_id
+    # force host name to FQDN
+    mapper._instances[host_name].host_name = getfqdn()
+    # find self
+    mapper.find_local_identifier()
     assert mapper.local_identifier == host_name
 
 
 def test_find_local_identifier_host_name(mapper):
-    """ Test the SupvisorsMapper.find_local_identifier method when one instance matches the host name. """
-    hostname = gethostname()
-    items = ['127.0.0.1', f'{hostname}:60000:7777']
-    mapper.configure(items, [])
-    assert mapper.local_identifier == f'{hostname}:60000'
+    """ Test the SupvisorsMapper.find_local_identifier method when one instance matches the host name and the HTTP
+    server port. """
+    host_name = gethostname()
+    items = ['127.0.0.1', f'{host_name}:65000:7777']
+    for item in items:
+        supvisors_id = SupvisorsInstanceId(item, mapper.supvisors)
+        mapper._instances[supvisors_id.identifier] = supvisors_id
+    # force host name to FQDN
+    mapper._instances[f'{host_name}:65000'].host_name = getfqdn()
+    # find self
+    mapper.find_local_identifier()
+    assert mapper.local_identifier == f'{host_name}:65000'
 
 
 def test_find_local_identifier_ip_address(mapper):
-    """ Test the SupvisorsMapper.find_local_identifier method when one instance matches the IP address of the host. """
-    hostname = gethostname()
-    mapper.local_node_references = [hostname, '10.0.0.1']
-    items = ['127.0.0.1', '<host>10.0.0.1:60000:7777']
-    mapper.configure(items, [])
+    """ Test the SupvisorsMapper.find_local_identifier method when one instance matches the IP address of the host
+    and the HTTP server port. """
+    host_name, _, ip_addresses = gethostbyaddr(gethostname())
+    items = ['127.0.0.1', f'<host>{ip_addresses[0]}:65000:7777']
+    for item in items:
+        supvisors_id = SupvisorsInstanceId(item, mapper.supvisors)
+        mapper._instances[supvisors_id.identifier] = supvisors_id
+    # force host name to FQDN
+    mapper._instances['host'].host_name = getfqdn()
+    # find self
+    mapper.find_local_identifier()
     assert mapper.local_identifier == 'host'
 
 
+def test_find_local_identifier_inconsistent(mapper):
+    """ Test the SupvisorsMapper.find_local_identifier method when the identifier corresponds to thr local Supvisors
+    instance but the host_name and http_port do not match. """
+    host_name = gethostname()
+    supvisors_id = SupvisorsInstanceId(f'<{host_name}>10.0.0.5:7777:', mapper.supvisors)
+    mapper._instances[supvisors_id.identifier] = supvisors_id
+    # find self
+    print(mapper.supvisors.supervisor_data.identifier)
+    with pytest.raises(ValueError):
+        mapper.find_local_identifier()
+
+
 def test_find_local_identifier_multiple(mapper):
     """ Test the SupvisorsMapper.find_local_identifier method when more than one instance matches the host name
     or IP address of the host. """
-    hostname = gethostname()
-    mapper.local_node_references = [hostname, '10.0.0.1']
-    items = ['10.0.0.1', f'{hostname}:60000:7777']
+    host_name = gethostname()
+    fqdn = getfqdn()
+    items = ['10.0.0.1', f'{host_name}:65000:7777']
+    for item in items:
+        supvisors_id = SupvisorsInstanceId(item, mapper.supvisors)
+        mapper._instances[supvisors_id.identifier] = supvisors_id
+        # update default processing of host
+        supvisors_id.host_name = fqdn
+    # find self
     with pytest.raises(ValueError):
-        mapper.configure(items, [])
+        mapper.find_local_identifier()
 
 
 def test_find_local_identifier_none(mapper):
-    """ Test the SupvisorsMapper.find_local_identifier method when more than no instance matches the host name
+    """ Test the SupvisorsMapper.find_local_identifier method when no instance matches the host name
     or IP address of the host. """
-    mapper.local_node_references = ['10.0.0.1']
-    items = ['10.0.0.2', '<dummy>cliche:60000:7777']
+    host_name = gethostname()
+    items = ['10.0.0.2', '<dummy>cliche:60000:7777', f'{host_name}:60000:7777']
+    for item in items:
+        supvisors_id = SupvisorsInstanceId(item, mapper.supvisors)
+        mapper._instances[supvisors_id.identifier] = supvisors_id
     with pytest.raises(ValueError):
-        mapper.configure(items, [])
+        mapper.find_local_identifier()
 
 
 def test_valid(mapper):
@@ -217,7 +261,11 @@ def test_valid(mapper):
     # add context
     hostname = gethostname()
     items = ['127.0.0.1', '<host>10.0.0.1:2222:', f'{hostname}:60000:7777']
-    mapper.configure(items, [])
+    for item in items:
+        supvisors_id = SupvisorsInstanceId(item, mapper.supvisors)
+        mapper._instances[supvisors_id.identifier] = supvisors_id
+    # force host name to FQDN
+    mapper._instances[f'{hostname}:60000'].host_name = getfqdn()
     # test calls
     assert mapper.valid('127.0.0.1')
     assert mapper.valid('host')
@@ -232,7 +280,11 @@ def test_filter(mapper):
     # add context
     hostname = gethostname()
     items = ['127.0.0.1', '<host>10.0.0.1:2222:', f'{hostname}:60000:7777']
-    mapper.configure(items, [])
+    for item in items:
+        supvisors_id = SupvisorsInstanceId(item, mapper.supvisors)
+        mapper._instances[supvisors_id.identifier] = supvisors_id
+    # force host name to FQDN
+    mapper._instances[f'{hostname}:60000'].host_name = getfqdn()
     # test with a bunch of identifiers
     identifier_list = ['127.0.0.1', 'host', f'{hostname}:60000', hostname, 'host', 'supervisor', '10.0.0.1']
     assert mapper.filter(identifier_list) == ['127.0.0.1', 'host', f'{hostname}:60000']
diff --git a/supvisors/tests/test_supvisorssocket.py b/supvisors/tests/test_supvisorssocket.py
index 0bc12281..b11ae0aa 100644
--- a/supvisors/tests/test_supvisorssocket.py
+++ b/supvisors/tests/test_supvisorssocket.py
@@ -1,7 +1,5 @@
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
-import socket
-import time
 
 # ======================================================================
 # Copyright 2022 Julien LE CLEACH
@@ -19,6 +17,8 @@
 # limitations under the License.
 # ======================================================================
 
+import socket
+
 import pytest
 
 from supvisors.supvisorssocket import *
@@ -184,10 +184,15 @@ def test_global_normal(supvisors, sockets):
     check_sockets(subscriber, None,
                   [InternalEventHeaders.PROCESS_DISABILITY.value, {'name': 'dummy_name', 'disabled': True}],
                   local_identifier, [hb_sent, hb_recv])
-    # test publish / subscribe for STATISTICS
-    publisher.send_statistics({'cpu': 25.3, 'mem': 12.5})
+    # test publish / subscribe for HOST_STATISTICS
+    publisher.send_host_statistics({'cpu': 25.3, 'mem': 12.5})
+    check_sockets(subscriber, None,
+                  [InternalEventHeaders.HOST_STATISTICS.value, {'cpu': 25.3, 'mem': 12.5}],
+                  local_identifier, [hb_sent, hb_recv])
+    # test publish / subscribe for PROCESS_STATISTICS
+    publisher.send_process_statistics({'dummy_process': {'cpu': 25.3, 'mem': 12.5}})
     check_sockets(subscriber, None,
-                  [InternalEventHeaders.STATISTICS.value, {'cpu': 25.3, 'mem': 12.5}],
+                  [InternalEventHeaders.PROCESS_STATISTICS.value, {'dummy_process': {'cpu': 25.3, 'mem': 12.5}}],
                   local_identifier, [hb_sent, hb_recv])
     # test publish / subscribe for STATE
     publisher.send_state_event({'state': 'operational', 'mode': 'starting'})
@@ -342,7 +347,7 @@ def test_publisher_bind_exception(supvisors):
 
 
 def test_publisher_forward_empty_message(supvisors, sockets):
-    """ . """
+    """ Test the robustness when the publisher forwards an empty message. """
     subscriber = sockets.subscriber
     publisher = sockets.publisher
     # wait for publisher server to be alive
diff --git a/supvisors/tests/test_supvisorswebsockets.py b/supvisors/tests/test_supvisorswebsockets.py
new file mode 100644
index 00000000..ee193d80
--- /dev/null
+++ b/supvisors/tests/test_supvisorswebsockets.py
@@ -0,0 +1,304 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# ======================================================================
+# Copyright 2023 Julien LE CLEACH
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ======================================================================
+
+import pytest
+pytest.importorskip('websockets', reason='cannot test as optional websockets is not installed')
+
+import json
+import time
+import websockets
+
+from unittest.mock import call
+
+from supvisors.client.wssubscriber import SupvisorsWsEventInterface
+from supvisors.supvisorswebsocket import WsEventPublisher, WsEventSubscriber, websocket_clients
+from supvisors.ttypes import EventHeaders
+
+
+@pytest.fixture
+def publisher(supvisors):
+    test_publisher = WsEventPublisher(supvisors.supvisors_mapper.local_instance, supvisors.logger)
+    yield test_publisher
+    test_publisher.close()
+
+
+@pytest.fixture
+def subscriber(mocker, supvisors):
+    test_subscriber = SupvisorsWsEventInterface('localhost', supvisors.options.event_port, supvisors.logger)
+    mocker.patch.object(test_subscriber, 'on_receive')
+    # do NOT start the thread to let subscriptions happen
+    yield test_subscriber
+    test_subscriber.stop()
+
+
+@pytest.fixture
+def real_subscriber(supvisors):
+    test_subscriber = SupvisorsWsEventInterface('localhost', supvisors.options.event_port, supvisors.logger)
+    test_subscriber.subscribe_all()
+    yield test_subscriber
+    test_subscriber.stop()
+
+
+supvisors_payload = {'state': 'running', 'version': '1.0'}
+instance_payload = {'state': 'silent', 'identifier': 'cliche01', 'date': 1234}
+application_payload = {'state': 'starting', 'name': 'supvisors'}
+event_payload = {'state': 20, 'name': 'plugin', 'group': 'supvisors', 'now': 1230}
+process_payload = {'state': 'running', 'process_name': 'plugin', 'application_name': 'supvisors', 'date': 1230}
+hstats_payload = {'identifier': '10.0.0.1', 'period': 5.2, 'uptime': 1230, 'cpu': [28.3]}
+pstats_payload = {'identifier': '10.0.0.1', 'namespec': 'dummy_proc', 'period': 5.2, 'uptime': 1230, 'cpu': [28.3]}
+
+
+def publish_all(publisher, close=False):
+    """ Send all kind of expected messages. """
+    publisher.send_supvisors_status(supvisors_payload)
+    publisher.send_instance_status(instance_payload)
+    publisher.send_application_status(application_payload)
+    publisher.send_process_event('local_identifier', event_payload)
+    publisher.send_process_status(process_payload)
+    publisher.send_host_statistics(hstats_payload)
+    publisher.send_process_statistics(pstats_payload)
+    if close:
+        publisher.close()
+
+
+def check_subscription(subscriber, publisher,
+                       supvisors_subscribed=False, instance_subscribed=False,
+                       application_subscribed=False, event_subscribed=False, process_subscribed=False,
+                       hstats_subscribed=False, pstats_subscribed=False):
+    """ The method tests the emission and reception of all status, depending on their subscription status. """
+    subscriber.start()
+    # give time to the websocket client to connect the server
+    time.sleep(2)
+    # publish and receive
+    publish_all(publisher)
+    # give time to the subscriber to receive data
+    time.sleep(2)
+    # get the results list expected
+    results = []
+    if supvisors_subscribed:
+        results.append(call(EventHeaders.SUPVISORS.value, supvisors_payload))
+    if instance_subscribed:
+        results.append(call(EventHeaders.INSTANCE.value, instance_payload))
+    if application_subscribed:
+        results.append(call(EventHeaders.APPLICATION.value, application_payload))
+    if event_subscribed:
+        payload = dict(event_payload, **{'identifier': 'local_identifier'})
+        results.append(call(EventHeaders.PROCESS_EVENT.value, payload))
+    if process_subscribed:
+        results.append(call(EventHeaders.PROCESS_STATUS.value, process_payload))
+    if hstats_subscribed:
+        results.append(call(EventHeaders.HOST_STATISTICS.value, hstats_payload))
+    if pstats_subscribed:
+        results.append(call(EventHeaders.PROCESS_STATISTICS.value, pstats_payload))
+    # check the results
+    assert subscriber.on_receive.call_args_list == results
+    subscriber.on_receive.reset_mock()
+    # stop and reset the subscriber
+    subscriber.stop()
+    subscriber.reset()
+
+
+def test_external_publish_subscribe(supvisors):
+    """ Test the opening and closing of the Websocket server/client used in the event interface of Supvisors. """
+    # get event port
+    port = supvisors.options.event_port
+    # create publisher and subscriber
+    publisher = WsEventPublisher(supvisors.supvisors_mapper.local_instance, supvisors.logger)
+    subscriber = SupvisorsWsEventInterface('localhost', port, supvisors.logger)
+    subscriber.start()
+    time.sleep(1)
+    # check the Server side
+    assert publisher.thread.is_alive()
+    assert publisher.thread.loop.is_running()
+    assert not publisher.thread.stop_event.is_set()
+    # sleep a bit to give time to hit the reception timeout
+    time.sleep(WsEventSubscriber.RecvTimeout)
+    # check the Client side
+    assert subscriber.headers == set()
+    assert subscriber.thread.is_alive()
+    assert subscriber.thread.loop.is_running()
+    assert not subscriber.thread.stop_event.is_set()
+    # close the sockets
+    publisher.close()
+    subscriber.stop()
+    # check the Server side
+    assert not publisher.thread.is_alive()
+    assert not publisher.thread.loop.is_running()
+    assert publisher.thread.stop_event.is_set()
+    # check the Client side
+    assert not subscriber.thread.is_alive()
+    assert not subscriber.thread.loop.is_running()
+    assert subscriber.thread.stop_event.is_set()
+
+
+def test_no_subscription(publisher, subscriber):
+    """ Test the non-reception of messages when subscription is not set. """
+    # at this stage, no subscription has been set so nothing should be received
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_supvisors_status(publisher, subscriber):
+    """ Test the reception of Supvisors status messages when related subscription is set. """
+    # subscribe to Supvisors status only
+    subscriber.subscribe_supvisors_status()
+    check_subscription(subscriber, publisher, supvisors_subscribed=True)
+    # unsubscribe from Supvisors status
+    subscriber.unsubscribe_supvisors_status()
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_instance_status(publisher, subscriber):
+    """ Test the reception of Instance status messages when related subscription is set. """
+    # subscribe to Instance status only
+    subscriber.subscribe_instance_status()
+    check_subscription(subscriber, publisher, instance_subscribed=True)
+    # unsubscribe from Instance status
+    subscriber.unsubscribe_instance_status()
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_application_status(publisher, subscriber):
+    """ Test the reception of Application status messages when related subscription is set. """
+    # subscribe to Application status only
+    subscriber.subscribe_application_status()
+    check_subscription(subscriber, publisher, application_subscribed=True)
+    # unsubscribe from Application status
+    subscriber.unsubscribe_application_status()
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_process_event(publisher, subscriber):
+    """ Test the reception of Process event messages when related subscription is set. """
+    # subscribe to Process event only
+    subscriber.subscribe_process_event()
+    check_subscription(subscriber, publisher, event_subscribed=True)
+    # unsubscribe from Process event
+    subscriber.unsubscribe_process_event()
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_process_status(publisher, subscriber):
+    """ Test the reception of Process status messages when related subscription is set. """
+    # subscribe to Process status only
+    subscriber.subscribe_process_status()
+    check_subscription(subscriber, publisher, process_subscribed=True)
+    # unsubscribe from Process status
+    subscriber.unsubscribe_process_status()
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_host_statistics(publisher, subscriber):
+    """ Test the reception of Host statistics messages when related subscription is set. """
+    # subscribe to Host statistics only
+    subscriber.subscribe_host_statistics()
+    check_subscription(subscriber, publisher, hstats_subscribed=True)
+    # unsubscribe from Host statistics
+    subscriber.unsubscribe_host_statistics()
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_process_statistics(publisher, subscriber):
+    """ Test the reception of Process statistics messages when related subscription is set. """
+    # subscribe to Process statistics only
+    subscriber.subscribe_process_statistics()
+    check_subscription(subscriber, publisher, pstats_subscribed=True)
+    # unsubscribe from Process statistics
+    subscriber.unsubscribe_process_statistics()
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_all_status(publisher, subscriber):
+    """ Test the reception of all status messages when related subscription is set. """
+    # subscribe to every status
+    subscriber.subscribe_all()
+    check_subscription(subscriber, publisher,
+                       supvisors_subscribed=True, instance_subscribed=True,
+                       application_subscribed=True, event_subscribed=True, process_subscribed=True,
+                       hstats_subscribed=True, pstats_subscribed=True)
+    # unsubscribe all
+    subscriber.unsubscribe_all()
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_multiple_status(publisher, subscriber):
+    """ Test the reception of multiple status messages when related subscription is set. """
+    # subscribe to Application and Process Event
+    subscriber.subscribe_application_status()
+    subscriber.subscribe_process_event()
+    check_subscription(subscriber, publisher, application_subscribed=True, event_subscribed=True)
+    # set subscription to Instance and Process Status
+    subscriber.reset()
+    subscriber.unsubscribe_application_status()
+    subscriber.unsubscribe_process_event()
+    subscriber.subscribe_process_status()
+    subscriber.subscribe_instance_status()
+    check_subscription(subscriber, publisher, instance_subscribed=True, process_subscribed=True)
+    # add subscription to Supvisors Status and remove process
+    subscriber.reset()
+    subscriber.subscribe_supvisors_status()
+    subscriber.unsubscribe_process_status()
+    check_subscription(subscriber, publisher, supvisors_subscribed=True, instance_subscribed=True)
+    # add subscription to Statistics and remove Supvisors Status
+    subscriber.reset()
+    subscriber.unsubscribe_supvisors_status()
+    subscriber.subscribe_host_statistics()
+    subscriber.subscribe_process_statistics()
+    check_subscription(subscriber, publisher, instance_subscribed=True, hstats_subscribed=True, pstats_subscribed=True)
+    # unsubscribe all
+    subscriber.reset()
+    subscriber.unsubscribe_instance_status()
+    subscriber.unsubscribe_host_statistics()
+    subscriber.unsubscribe_process_statistics()
+    check_subscription(subscriber, publisher)
+
+
+def test_unknown_message(mocker, publisher, real_subscriber):
+    """ Test the reception of a message with unknown header. """
+    # give time to the websocket client to connect the server
+    real_subscriber.start()
+    time.sleep(2)
+    # mock the on_xxx methods
+    mocked_ons = [mocker.patch.object(real_subscriber, method_name)
+                  for method_name in ['on_supvisors_status', 'on_instance_status', 'on_application_status',
+                                      'on_process_event', 'on_process_status',
+                                      'on_host_statistics', 'on_process_statistics']]
+    # publish a message of each type
+    publish_all(publisher)
+    time.sleep(2)
+    # check that no NotImplementedError exception is raised and all on_xxx called
+    for mock in mocked_ons:
+        assert mock.call_count == 1
+        mock.reset_mock()
+    # now send an unexpected event
+    websockets.broadcast(websocket_clients, json.dumps(('dummy header', 'dummy body')))
+    # check that no on_xxx has been called
+    time.sleep(2)
+    assert all(not mock.called for mock in mocked_ons)
+
+
+def test_close_server(real_subscriber, publisher):
+    """ Test the server closure while a client is connected. """
+    # give time to the websocket client to connect the server
+    real_subscriber.start()
+    time.sleep(2)
+    # the publisher will be stopped just after all the publications
+    # default websocket ping is 20 seconds
+    publish_all(publisher, True)
+    time.sleep(30)
+    # the ConnectionClosed handling should have been hit
diff --git a/supvisors/tests/test_supvisorszmq.py b/supvisors/tests/test_supvisorszmq.py
index 855552fe..41217384 100644
--- a/supvisors/tests/test_supvisorszmq.py
+++ b/supvisors/tests/test_supvisorszmq.py
@@ -18,191 +18,213 @@
 # ======================================================================
 
 import pytest
+pytest.importorskip('zmq', reason='cannot test as optional pyzmq is not installed')
 
-from supvisors.supvisorszmq import *
-from supvisors.ttypes import DeferredRequestHeaders
-from time import sleep
-from unittest.mock import call, Mock
+import time
 
+from time import sleep
+from unittest.mock import call
 
-def test_external_publish_subscribe(supvisors):
-    """ Test the ZeroMQ publish-subscribe sockets used in the event interface of Supvisors. """
-    # get event port
-    port = supvisors.options.event_port
-    # create publisher and subscriber
-    publisher = EventPublisher(supvisors.supvisors_mapper.local_instance, supvisors.logger)
-    subscriber = EventSubscriber(zmq.Context.instance(), port, supvisors.logger)
-    # check that the ZMQ sockets are ready
-    assert not publisher.socket.closed
-    assert not subscriber.socket.closed
-    # close the sockets
-    publisher.close()
-    subscriber.close()
-    # check that the ZMQ socket are closed
-    assert publisher.socket.closed
-    assert subscriber.socket.closed
+from supvisors.client.zmqsubscriber import SupvisorsZmqEventInterface
+from supvisors.supvisorszmq import *
 
 
 @pytest.fixture
 def publisher(supvisors):
-    test_publisher = EventPublisher(supvisors.supvisors_mapper.local_instance, supvisors.logger)
+    test_publisher = ZmqEventPublisher(supvisors.supvisors_mapper.local_instance, supvisors.logger)
     yield test_publisher
     test_publisher.close()
     sleep(0.5)
 
 
 @pytest.fixture
-def subscriber(supvisors):
-    test_subscriber = EventSubscriber(zmq.Context.instance(), supvisors.options.event_port, supvisors.logger)
-    # WARN: this subscriber does not include a subscription
-    # when using a subscription, use a time sleep to give time to PyZMQ to handle it
-    # sleep(0.5)
-    # WARN: socket configuration is meant to be blocking
-    # however, a failure would block the unit test, so a timeout is set for reception
-    test_subscriber.socket.setsockopt(zmq.RCVTIMEO, 1000)
+def subscriber(mocker, supvisors):
+    test_subscriber = SupvisorsZmqEventInterface(zmq.asyncio.Context.instance(), 'localhost', supvisors.options.event_port,
+                                                 supvisors.logger)
+    mocker.patch.object(test_subscriber, 'on_receive')
     yield test_subscriber
-    test_subscriber.close()
+    test_subscriber.stop()
     sleep(0.5)
 
 
-def check_reception(subscriber, header=None, data=None):
-    """ The method tests that the message is received correctly or not received at all. """
-    if header and data:
-        # check that subscriber receives the message
-        msg = subscriber.receive()
-        assert msg == (header.value, data)
-    else:
-        # check the non-reception of the Supvisors status
-        with pytest.raises(zmq.Again):
-            subscriber.receive()
+@pytest.fixture
+def real_subscriber(supvisors):
+    test_subscriber = SupvisorsZmqEventInterface(zmq.asyncio.Context.instance(),
+                                                 'localhost', supvisors.options.event_port,
+                                                 supvisors.logger)
+    test_subscriber.subscribe_all()
+    yield test_subscriber
+    test_subscriber.stop()
 
 
-def check_supvisors_status(subscriber, publisher, subscribed):
-    """ The method tests the emission and reception of a Supvisors status, depending on the subscription status. """
-    supvisors_payload = {'state': 'running', 'version': '1.0'}
-    publisher.send_supvisors_status(supvisors_payload)
-    if subscribed:
-        check_reception(subscriber, EventHeaders.SUPVISORS, supvisors_payload)
-    else:
-        check_reception(subscriber)
+def test_external_publish_subscribe(supvisors):
+    """ Test the ZeroMQ publish-subscribe sockets used in the event interface of Supvisors. """
+    # create publisher and subscriber
+    publisher = ZmqEventPublisher(supvisors.supvisors_mapper.local_instance, supvisors.logger)
+    subscriber = SupvisorsZmqEventInterface(zmq.asyncio.Context.instance(), 'localhost', supvisors.options.event_port,
+                                            supvisors.logger)
+    subscriber.start()
+    time.sleep(1)
+    # check that the ZMQ sockets are ready
+    assert not publisher.socket.closed
+    # check the Client side
+    assert subscriber.headers == set()
+    assert subscriber.thread.is_alive()
+    assert subscriber.thread.loop.is_running()
+    assert not subscriber.thread.stop_event.is_set()
+    # close the sockets and stop the reception thread
+    publisher.close()
+    subscriber.stop()
+    # check that the ZMQ socket are closed
+    assert publisher.socket.closed
+    # check the Client side
+    assert not subscriber.thread.is_alive()
+    assert not subscriber.thread.loop.is_running()
+    assert subscriber.thread.stop_event.is_set()
 
 
-def check_instance_status(subscriber, publisher, subscribed):
-    """ The method tests the emission and reception of an node status, depending on the subscription status. """
-    node_payload = {'state': 'silent', 'identifier': 'cliche01', 'date': 1234}
-    publisher.send_instance_status(node_payload)
-    if subscribed:
-        check_reception(subscriber, EventHeaders.INSTANCE, node_payload)
-    else:
-        check_reception(subscriber)
+supvisors_payload = {'state': 'running', 'version': '1.0'}
+instance_payload = {'state': 'silent', 'identifier': 'cliche01', 'date': 1234}
+application_payload = {'state': 'starting', 'name': 'supvisors'}
+event_payload = {'state': 20, 'name': 'plugin', 'group': 'supvisors', 'now': 1230}
+process_payload = {'state': 'running', 'process_name': 'plugin', 'application_name': 'supvisors', 'date': 1230}
+hstats_payload = {'identifier': '10.0.0.1', 'period': 5.2, 'uptime': 1230, 'cpu': [28.3]}
+pstats_payload = {'identifier': '10.0.0.1', 'namespec': 'dummy_proc', 'period': 5.2, 'uptime': 1230, 'cpu': [28.3]}
 
 
-def check_application_status(subscriber, publisher, subscribed):
-    """ The method tests the emission and reception of an Application status, depending on the subscription status. """
-    application_payload = {'state': 'starting', 'name': 'supvisors'}
+def publish_all(publisher):
+    """ Send all kind of expected messages. """
+    publisher.send_supvisors_status(supvisors_payload)
+    publisher.send_instance_status(instance_payload)
     publisher.send_application_status(application_payload)
-    if subscribed:
-        check_reception(subscriber, EventHeaders.APPLICATION, application_payload)
-    else:
-        check_reception(subscriber)
-
-
-def check_process_event(subscriber, publisher, subscribed):
-    """ The method tests the emission and reception of a Process status,  depending on the subscription status. """
-    event_payload = {'state': 20, 'name': 'plugin', 'group': 'supvisors', 'now': 1230}
     publisher.send_process_event('local_identifier', event_payload)
-    if subscribed:
-        event_payload['identifier'] = 'local_identifier'
-        check_reception(subscriber, EventHeaders.PROCESS_EVENT, event_payload)
-    else:
-        check_reception(subscriber)
-
-
-def check_process_status(subscriber, publisher, subscribed):
-    """ The method tests the emission and reception of a Process status, depending on the subscription status. """
-    process_payload = {'state': 'running', 'process_name': 'plugin', 'application_name': 'supvisors', 'date': 1230}
     publisher.send_process_status(process_payload)
-    if subscribed:
-        check_reception(subscriber, EventHeaders.PROCESS_STATUS, process_payload)
-    else:
-        check_reception(subscriber)
+    publisher.send_host_statistics(hstats_payload)
+    publisher.send_process_statistics(pstats_payload)
 
 
-def check_subscription(subscriber, publisher, supvisors_subscribed, address_subscribed,
-                       application_subscribed, event_subscribed, process_subscribed):
+def check_subscription(subscriber, publisher, supvisors_subscribed=False, instance_subscribed=False,
+                       application_subscribed=False, event_subscribed=False, process_subscribed=False,
+                       hstats_subscribed=False, pstats_subscribed=False):
     """ The method tests the emission and reception of all status, depending on their subscription status. """
-    sleep(1)
-    check_supvisors_status(subscriber, publisher, supvisors_subscribed)
-    check_instance_status(subscriber, publisher, address_subscribed)
-    check_application_status(subscriber, publisher, application_subscribed)
-    check_process_event(subscriber, publisher, event_subscribed)
-    check_process_status(subscriber, publisher, process_subscribed)
+    subscriber.start()
+    # give time to the websocket client to connect the server
+    time.sleep(2)
+    # publish and receive
+    publish_all(publisher)
+    # give time to the subscriber to receive data
+    time.sleep(2)
+    # get the results list expected
+    results = []
+    if supvisors_subscribed:
+        results.append(call(EventHeaders.SUPVISORS.value, supvisors_payload))
+    if instance_subscribed:
+        results.append(call(EventHeaders.INSTANCE.value, instance_payload))
+    if application_subscribed:
+        results.append(call(EventHeaders.APPLICATION.value, application_payload))
+    if event_subscribed:
+        payload = dict(event_payload, **{'identifier': 'local_identifier'})
+        results.append(call(EventHeaders.PROCESS_EVENT.value, payload))
+    if process_subscribed:
+        results.append(call(EventHeaders.PROCESS_STATUS.value, process_payload))
+    if hstats_subscribed:
+        results.append(call(EventHeaders.HOST_STATISTICS.value, hstats_payload))
+    if pstats_subscribed:
+        results.append(call(EventHeaders.PROCESS_STATISTICS.value, pstats_payload))
+    # check the results
+    assert subscriber.on_receive.call_args_list == results
+    subscriber.on_receive.reset_mock()
+    # stop and reset the subscriber
+    subscriber.stop()
+    subscriber.reset()
 
 
 def test_no_subscription(publisher, subscriber):
     """ Test the non-reception of messages when subscription is not set. """
     # at this stage, no subscription has been set so nothing should be received
-    check_subscription(subscriber, publisher, False, False, False, False, False)
+    check_subscription(subscriber, publisher)
 
 
 def test_subscription_supvisors_status(publisher, subscriber):
     """ Test the reception of Supvisors status messages when related subscription is set. """
     # subscribe to Supvisors status only
     subscriber.subscribe_supvisors_status()
-    check_subscription(subscriber, publisher, True, False, False, False, False)
+    check_subscription(subscriber, publisher, supvisors_subscribed=True)
     # unsubscribe from Supvisors status
     subscriber.unsubscribe_supvisors_status()
-    check_subscription(subscriber, publisher, False, False, False, False, False)
+    check_subscription(subscriber, publisher)
 
 
-def test_subscription_node_status(publisher, subscriber):
+def test_subscription_instance_status(publisher, subscriber):
     """ Test the reception of Address status messages when related subscription is set. """
-    # subscribe to Address status only
+    # subscribe to Instance status only
     subscriber.subscribe_instance_status()
-    check_subscription(subscriber, publisher, False, True, False, False, False)
-    # unsubscribe from Address status
+    check_subscription(subscriber, publisher, instance_subscribed=True)
+    # unsubscribe from Instance status
     subscriber.unsubscribe_instance_status()
-    check_subscription(subscriber, publisher, False, False, False, False, False)
+    check_subscription(subscriber, publisher)
 
 
 def test_subscription_application_status(publisher, subscriber):
     """ Test the reception of Application status messages when related subscription is set. """
     # subscribe to Application status only
     subscriber.subscribe_application_status()
-    check_subscription(subscriber, publisher, False, False, True, False, False)
+    check_subscription(subscriber, publisher, application_subscribed=True)
     # unsubscribe from Application status
     subscriber.unsubscribe_application_status()
-    check_subscription(subscriber, publisher, False, False, False, False, False)
+    check_subscription(subscriber, publisher)
 
 
 def test_subscription_process_event(publisher, subscriber):
     """ Test the reception of Process event messages when related subscription is set. """
     # subscribe to Process event only
     subscriber.subscribe_process_event()
-    check_subscription(subscriber, publisher, False, False, False, True, False)
+    check_subscription(subscriber, publisher, event_subscribed=True)
     # unsubscribe from Process event
     subscriber.unsubscribe_process_event()
-    check_subscription(subscriber, publisher, False, False, False, False, False)
+    check_subscription(subscriber, publisher)
 
 
 def test_subscription_process_status(publisher, subscriber):
     """ Test the reception of Process status messages when related subscription is set. """
     # subscribe to Process status only
     subscriber.subscribe_process_status()
-    check_subscription(subscriber, publisher, False, False, False, False, True)
+    check_subscription(subscriber, publisher, process_subscribed=True)
     # unsubscribe from Process status
     subscriber.unsubscribe_process_status()
-    check_subscription(subscriber, publisher, False, False, False, False, False)
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_host_statistics(publisher, subscriber):
+    """ Test the reception of Host statistics messages when related subscription is set. """
+    # subscribe to Host statistics only
+    subscriber.subscribe_host_statistics()
+    check_subscription(subscriber, publisher, hstats_subscribed=True)
+    # unsubscribe from Host statistics
+    subscriber.unsubscribe_host_statistics()
+    check_subscription(subscriber, publisher)
+
+
+def test_subscription_process_statistics(publisher, subscriber):
+    """ Test the reception of Process statistics messages when related subscription is set. """
+    # subscribe to Process statistics only
+    subscriber.subscribe_process_statistics()
+    check_subscription(subscriber, publisher, pstats_subscribed=True)
+    # unsubscribe from Process statistics
+    subscriber.unsubscribe_process_statistics()
+    check_subscription(subscriber, publisher)
 
 
 def test_subscription_all_status(publisher, subscriber):
     """ Test the reception of all status messages when related subscription is set. """
     # subscribe to every status
     subscriber.subscribe_all()
-    check_subscription(subscriber, publisher, True, True, True, True, True)
+    check_subscription(subscriber, publisher, supvisors_subscribed=True, instance_subscribed=True,
+                       application_subscribed=True, event_subscribed=True, process_subscribed=True,
+                       hstats_subscribed=True, pstats_subscribed=True)
     # unsubscribe all
     subscriber.unsubscribe_all()
-    check_subscription(subscriber, publisher, False, False, False, False, False)
+    check_subscription(subscriber, publisher)
 
 
 def test_subscription_multiple_status(publisher, subscriber):
@@ -210,18 +232,85 @@ def test_subscription_multiple_status(publisher, subscriber):
     # subscribe to Application and Process Event
     subscriber.subscribe_application_status()
     subscriber.subscribe_process_event()
-    check_subscription(subscriber, publisher, False, False, True, True, False)
-    # set subscription to Node and Process Status
+    check_subscription(subscriber, publisher, application_subscribed=True, event_subscribed=True)
+    # set subscription to Instance and Process Status
     subscriber.unsubscribe_application_status()
     subscriber.unsubscribe_process_event()
     subscriber.subscribe_process_status()
     subscriber.subscribe_instance_status()
-    check_subscription(subscriber, publisher, False, True, False, False, True)
-    # add subscription to Supvisors Status
+    check_subscription(subscriber, publisher, instance_subscribed=True, process_subscribed=True)
+    # add subscription to Supvisors Status and remove process
     subscriber.subscribe_supvisors_status()
-    check_subscription(subscriber, publisher, True, True, False, False, True)
-    # unsubscribe all
+    subscriber.unsubscribe_process_status()
+    check_subscription(subscriber, publisher, supvisors_subscribed=True, instance_subscribed=True)
+    # add subscription to Statistics and remove Supvisors Status
     subscriber.unsubscribe_supvisors_status()
+    subscriber.subscribe_host_statistics()
+    subscriber.subscribe_process_statistics()
+    check_subscription(subscriber, publisher, instance_subscribed=True, hstats_subscribed=True, pstats_subscribed=True)
+    # unsubscribe all
     subscriber.unsubscribe_instance_status()
-    subscriber.unsubscribe_process_status()
-    check_subscription(subscriber, publisher, False, False, False, False, False)
+    subscriber.unsubscribe_host_statistics()
+    subscriber.unsubscribe_process_statistics()
+    check_subscription(subscriber, publisher)
+
+
+def test_unknown_message(mocker, publisher, real_subscriber):
+    """ Test the reception of a message with unknown header. """
+    # give time to the websocket client to connect the server
+    real_subscriber.start()
+    time.sleep(2)
+    # mock the on_xxx methods
+    mocked_ons = [mocker.patch.object(real_subscriber, method_name)
+                  for method_name in ['on_supvisors_status', 'on_instance_status', 'on_application_status',
+                                      'on_process_event', 'on_process_status',
+                                      'on_host_statistics', 'on_process_statistics']]
+    # publish a message of each type
+    publish_all(publisher)
+    time.sleep(2)
+    # check that no NotImplementedError exception is raised and all on_xxx called
+    for mock in mocked_ons:
+        assert mock.call_count == 1
+        mock.reset_mock()
+    # now send an unexpected event
+    publisher.socket.send_string('dummy header', zmq.SNDMORE)
+    publisher.socket.send_json('dummy body')
+    # check that no on_xxx has been called
+    time.sleep(2)
+    assert all(not mock.called for mock in mocked_ons)
+
+
+def test_erroneous_message(mocker, publisher, real_subscriber):
+    """ Test the reception of a message with unknown header. """
+    # give time to the websocket client to connect the server
+    real_subscriber.start()
+    time.sleep(2)
+    # mock the on_xxx methods
+    mocked_ons = [mocker.patch.object(real_subscriber, method_name)
+                  for method_name in ['on_supvisors_status', 'on_instance_status', 'on_application_status',
+                                      'on_process_event', 'on_process_status',
+                                      'on_host_statistics', 'on_process_statistics']]
+    # send a message that is incompatible from a string or JSON point of view
+    publisher.socket.send(int(1234).to_bytes(8, 'big'))
+    # check that no on_xxx has been called
+    time.sleep(2)
+    assert all(not mock.called for mock in mocked_ons)
+
+
+def test_erroneous_socket(mocker, publisher, real_subscriber):
+    """ Test the reception of a message with unknown header. """
+    # patch the pyzmq recv_json
+    mocker.patch('zmq.sugar.socket.Socket.recv_json', side_effect=zmq.ZMQError)
+    # give time to the websocket client to connect the server
+    real_subscriber.start()
+    time.sleep(2)
+    # mock the on_xxx methods
+    mocked_ons = [mocker.patch.object(real_subscriber, method_name)
+                  for method_name in ['on_supvisors_status', 'on_instance_status', 'on_application_status',
+                                      'on_process_event', 'on_process_status',
+                                      'on_host_statistics', 'on_process_statistics']]
+    # send a message part. no need to send the body because recv_json has been mocked and won't read the socket
+    publisher.socket.send_string('dummy header')
+    # check that no on_xxx has been called
+    time.sleep(2)
+    assert all(not mock.called for mock in mocked_ons)
diff --git a/supvisors/tests/test_ttypes.py b/supvisors/tests/test_ttypes.py
index 0f4c925c..d0b2b7d6 100644
--- a/supvisors/tests/test_ttypes.py
+++ b/supvisors/tests/test_ttypes.py
@@ -45,7 +45,7 @@ def test_application_states():
 
 def test_event_links():
     """ Test the EventLinks enumeration. """
-    expected = ['NONE', 'ZMQ']
+    expected = ['NONE', 'ZMQ', 'WS']
     assert [x.name for x in EventLinks] == expected
 
 
@@ -73,10 +73,28 @@ def test_running_failure_strategies():
     assert [x.name for x in RunningFailureStrategies] == expected
 
 
+def test_process_request_result():
+    """ Test the ProcessRequestResult enumeration. """
+    expected = ['IN_PROGRESS', 'SUCCESS', 'FAILED', 'TIMED_OUT']
+    assert [x.name for x in ProcessRequestResult] == expected
+
+
+def test_distribution_rules():
+    """ Test the DistributionRules enumeration. """
+    expected = ['ALL_INSTANCES', 'SINGLE_INSTANCE', 'SINGLE_NODE']
+    assert [x.name for x in DistributionRules] == expected
+
+
+def test_statistics_types():
+    """ Test the StatisticsTypes enumeration. """
+    expected = ['OFF', 'HOST', 'PROCESS', 'ALL']
+    assert [x.name for x in StatisticsTypes] == expected
+
+
 def test_internal_event_headers():
     """ Test the InternalEventHeaders enumeration. """
     expected = ['HEARTBEAT', 'TICK', 'PROCESS', 'PROCESS_ADDED', 'PROCESS_REMOVED', 'PROCESS_DISABILITY',
-                'STATISTICS', 'STATE']
+                'HOST_STATISTICS', 'PROCESS_STATISTICS', 'STATE']
     assert [x.name for x in InternalEventHeaders] == expected
 
 
@@ -95,7 +113,8 @@ def test_remote_comm_events():
 
 def test_event_headers():
     """ Test the RemoteCommEvents enumeration. """
-    expected = ['SUPVISORS', 'INSTANCE', 'APPLICATION', 'PROCESS_EVENT', 'PROCESS_STATUS']
+    expected = ['SUPVISORS', 'INSTANCE', 'APPLICATION', 'PROCESS_EVENT', 'PROCESS_STATUS',
+                'HOST_STATISTICS', 'PROCESS_STATISTICS']
     assert [x.name for x in EventHeaders] == expected
 
 
diff --git a/supvisors/tests/test_utils.py b/supvisors/tests/test_utils.py
index 366306de..6b51e856 100644
--- a/supvisors/tests/test_utils.py
+++ b/supvisors/tests/test_utils.py
@@ -66,33 +66,19 @@ def test_extract_process_info():
 def test_server_url():
     """ Test the SupervisorServerUrl class. """
     # test without authentication
-    env = {'SUPERVISOR_SERVER_URL': 'http://localhost:60000'}
+    env = {'SUPERVISOR_SERVER_URL': 'http://localhost:60000', 'DUMMY': 'dummy'}
     srv_url = SupervisorServerUrl(env)
-    assert srv_url.env is env
-    assert srv_url.parsed_url.geturl() == 'http://localhost:60000'
+    assert srv_url.env == env
     assert srv_url.authentication == ''
-    # without port
-    srv_url.update_url('cliche81')
-    assert env['SUPERVISOR_SERVER_URL'] == 'http://cliche81:60000'
-    assert srv_url.parsed_url.geturl() == 'http://cliche81:60000'
-    # with port
-    srv_url.update_url('cliche82', 61000)
-    assert env['SUPERVISOR_SERVER_URL'] == 'http://cliche82:61000'
-    assert srv_url.parsed_url.geturl() == 'http://cliche82:61000'
+    srv_url.update_url('10.0.0.1', 7777)
+    assert srv_url.env == {'SUPERVISOR_SERVER_URL': 'http://10.0.0.1:7777', 'DUMMY': 'dummy'}
     # test with authentication
-    env = {'SUPERVISOR_SERVER_URL': 'http://user:password@localhost:60000'}
+    env = {'SUPERVISOR_SERVER_URL': 'http://user:password@localhost:60000', 'DUMMY': 'dummy'}
     srv_url = SupervisorServerUrl(env)
-    assert srv_url.env is env
-    assert srv_url.parsed_url.geturl() == 'http://user:password@localhost:60000'
+    assert srv_url.env == env
     assert srv_url.authentication == 'user:password@'
-    # without port
-    srv_url.update_url('cliche81')
-    assert env['SUPERVISOR_SERVER_URL'] == 'http://user:password@cliche81:60000'
-    assert srv_url.parsed_url.geturl() == 'http://user:password@cliche81:60000'
-    # with port
-    srv_url.update_url('cliche82', 61000)
-    assert env['SUPERVISOR_SERVER_URL'] == 'http://user:password@cliche82:61000'
-    assert srv_url.parsed_url.geturl() == 'http://user:password@cliche82:61000'
+    srv_url.update_url('10.0.0.1', 7777)
+    assert srv_url.env == {'SUPERVISOR_SERVER_URL': 'http://user:password@10.0.0.1:7777', 'DUMMY': 'dummy'}
 
 
 def test_statistics_functions():
@@ -132,10 +118,6 @@ def test_linear_regression_numpy():
     a, b = get_linear_regression(xdata, ydata)
     assert pytest.approx(a) == 0.5
     assert pytest.approx(b) == 2.0
-    # test simple linear regression
-    a, b = get_simple_linear_regression(ydata)
-    assert pytest.approx(a) == 1.0
-    assert pytest.approx(b) == 3.0
 
 
 def test_linear_regression(mocker):
@@ -147,20 +129,17 @@ def test_linear_regression(mocker):
     a, b = get_linear_regression(xdata, ydata)
     assert pytest.approx(a) == 0.5
     assert pytest.approx(b) == 2.0
-    # test simple linear regression
-    a, b = get_simple_linear_regression(ydata)
-    assert pytest.approx(a) == 1.0
-    assert pytest.approx(b) == 3.0
 
 
 def test_statistics():
     """ Test the statistics function. """
+    xdata = [2, 4, 6, 8, 10]
     ydata = [2, 3, 4, 5, 6]
-    avg, rate, (a, b), dev = get_stats(ydata)
+    avg, rate, (a, b), dev = get_stats(xdata, ydata)
     assert pytest.approx(avg) == 4
     assert pytest.approx(rate) == 20
-    assert pytest.approx(a) == 1
-    assert pytest.approx(b) == 2
+    assert pytest.approx(a) == 0.5
+    assert pytest.approx(b) == 1
     assert pytest.approx(dev) == math.sqrt(2)
 
 
diff --git a/supvisors/tests/test_viewapplication.py b/supvisors/tests/test_viewapplication.py
index 27487bed..6d2814d8 100644
--- a/supvisors/tests/test_viewapplication.py
+++ b/supvisors/tests/test_viewapplication.py
@@ -85,7 +85,7 @@ def test_write_navigation(mocker, view):
 def test_write_header(mocker, view):
     """ Test the write_header method. """
     mocked_action = mocker.patch('supvisors.viewapplication.ApplicationView.write_application_actions')
-    mocked_period = mocker.patch('supvisors.viewhandler.ViewHandler.write_periods')
+    mocked_period = mocker.patch('supvisors.viewapplication.ApplicationView.write_periods')
     mocked_strategy = mocker.patch('supvisors.viewapplication.ApplicationView.write_starting_strategy')
     view.application_name = 'dummy_appli'
     view.application = Mock(state=ApplicationStates.STOPPED, major_failure=False, minor_failure=False,
@@ -139,6 +139,20 @@ def test_write_header(mocker, view):
     assert mocked_action.call_args_list == [call(mocked_root)]
 
 
+def test_write_periods(mocker, view):
+    """ Test the ApplicationView.write_periods method. """
+    mocked_period = mocker.patch('supvisors.viewhandler.ViewHandler.write_periods_availability')
+    mocked_root = Mock()
+    # test with process statistics to be displayed
+    view.write_periods(mocked_root)
+    assert mocked_period.call_args_list == [call(mocked_root, True)]
+    mocked_period.reset_mock()
+    # test with process statistics NOT to be displayed
+    view.has_process_statistics = False
+    view.write_periods(mocked_root)
+    assert mocked_period.call_args_list == [call(mocked_root, False)]
+
+
 def test_write_starting_strategy(view):
     """ Test the write_starting_strategy method. """
     # patch the view context
diff --git a/supvisors/tests/test_viewcontext.py b/supvisors/tests/test_viewcontext.py
index 0e572bf4..470bbdb1 100644
--- a/supvisors/tests/test_viewcontext.py
+++ b/supvisors/tests/test_viewcontext.py
@@ -18,7 +18,7 @@
 # ======================================================================
 
 import re
-from unittest.mock import call, patch, Mock
+from unittest.mock import call, Mock
 
 import pytest
 
@@ -37,7 +37,7 @@ def http_context(supvisors):
 
 
 @pytest.fixture
-def ctx(mocker, http_context):
+def ctx(http_context):
     """ Fixture for the instance to test. """
     return ViewContext(http_context)
 
@@ -80,8 +80,8 @@ def test_get_gravity(ctx):
 def test_url_parameters(ctx):
     """ Test the ViewContext.url_parameters method. """
     # test default
-    assert ctx.url_parameters(False) == 'ident=10.0.0.4&period=5&strategy=CONFIG'
-    assert ctx.url_parameters(True) == 'ident=10.0.0.4&period=5&strategy=CONFIG'
+    assert ctx.url_parameters(False) == 'ident=10.0.0.4&period=5.0&strategy=CONFIG'
+    assert ctx.url_parameters(True) == 'ident=10.0.0.4&period=5.0&strategy=CONFIG'
     # update internal parameters
     ctx.parameters.update({'processname': 'dummy_proc', 'namespec': 'dummy_ns', 'ident': '10.0.0.1', 'cpuid': 3,
                            'intfname': 'eth0', 'appliname': 'dummy_appli', 'period': 8, 'strategy': 'CONFIG',
@@ -203,6 +203,45 @@ def test_update_integer(ctx):
     assert ctx.parameters == ref_parameters
 
 
+def test_update_float(ctx):
+    """ Test the ViewContext._update_float method. """
+    # keep a copy of parameters
+    ref_parameters = ctx.parameters.copy()
+    # test with unknown parameter and no default value
+    assert 'dummy' not in ctx.http_context.form
+    ctx._update_float('dummy', [])
+    ref_parameters.update({'dummy': 0})
+    assert ctx.parameters == ref_parameters
+    # test with unknown parameter and default value
+    assert 'dummy' not in ctx.http_context.form
+    ctx._update_float('dummy', [], 'hello')
+    ref_parameters.update({'dummy': 'hello'})
+    assert ctx.parameters == ref_parameters
+    # test with known parameter, not float and no default value
+    assert 'action' in ctx.http_context.form
+    ctx._update_float('action', [])
+    ref_parameters.update({'action': 0.0})
+    # test with known parameter, not float and default value
+    assert 'action' in ctx.http_context.form
+    ctx._update_float('action', [], 5.0)
+    ref_parameters.update({'action': 5.0})
+    # test with known parameter, float, no default value and value out of list
+    assert 'period' in ctx.http_context.form
+    ctx._update_float('period', [])
+    ref_parameters.update({'period': 0.0})
+    assert ctx.parameters == ref_parameters
+    # test with known parameter, float, default value and value out of list
+    assert 'period' in ctx.http_context.form
+    ctx._update_float('period', [], 12.3)
+    ref_parameters.update({'period': 12.3})
+    assert ctx.parameters == ref_parameters
+    # test with known parameter, float and value in list
+    assert 'period' in ctx.http_context.form
+    ctx._update_float('period', [5.1, 15.0, 60.0])
+    ref_parameters.update({'period': 5.1})
+    assert ctx.parameters == ref_parameters
+
+
 def test_update_boolean(ctx):
     """ Test the ViewContext._update_boolean method. """
     # keep a copy of parameters
@@ -235,7 +274,7 @@ def test_update_boolean(ctx):
 
 def test_update_period(mocker, ctx):
     """ Test the ViewContext.update_period method. """
-    mocked_update = mocker.patch('supvisors.viewcontext.ViewContext._update_integer')
+    mocked_update = mocker.patch('supvisors.viewcontext.ViewContext._update_float')
     # test method with statistics enabled
     ctx.update_period()
     assert mocked_update.call_args_list == [call(PERIOD, ctx.supvisors.options.stats_periods,
@@ -344,7 +383,7 @@ def test_update_namespec(mocker, ctx):
 
 def test_update_cpu_id(mocker, ctx):
     """ Test the ViewContext.update_cpu_id method. """
-    mocker.patch('supvisors.viewcontext.ViewContext.get_nbcores', return_value=2)
+    mocker.patch('supvisors.viewcontext.ViewContext.get_nb_cores', return_value=2)
     mocked_update = mocker.patch('supvisors.viewcontext.ViewContext._update_integer')
     # test call
     ctx.update_cpu_id()
@@ -377,14 +416,17 @@ def test_update_interface_name(mocker, ctx):
 def test_format_url(ctx):
     """ Test the ViewContext.format_url method. """
     # test without node and arguments
-    assert ctx.format_url(None, 'index.html') == 'index.html?ident=10.0.0.4&period=5&strategy=CONFIG'
+    assert ctx.format_url(None, 'index.html') == 'index.html?ident=10.0.0.4&period=5.0&strategy=CONFIG'
     # test with local node and arguments
-    base_address = f'http://{ctx.local_identifier}:65000/index.html?'
-    url = ctx.format_url(ctx.local_identifier, 'index.html', **{'period': 10, 'appliname': 'dummy_appli', 'shex': 'args'})
+    local_instance = ctx.supvisors.supvisors_mapper.local_instance
+    base_address = f'http://{local_instance.host_id}:65000/index.html?'
+    url = ctx.format_url(ctx.local_identifier, 'index.html',
+                         **{'period': 10, 'appliname': 'dummy_appli', 'shex': 'args'})
     expected = 'appliname=dummy_appli&ident=10.0.0.4&period=10&shex=args&strategy=CONFIG'
     assert url == base_address + expected
     # test with remote node and arguments (shex expected to be removed)
-    url = ctx.format_url('10.0.0.1', 'index.html', **{'period': 10, 'appliname': 'dummy_appli', 'shex': 'args'})
+    url = ctx.format_url('10.0.0.1', 'index.html',
+                         **{'period': 10, 'appliname': 'dummy_appli', 'shex': 'args'})
     base_address = 'http://10.0.0.1:65000/index.html?'
     expected = 'appliname=dummy_appli&ident=10.0.0.4&period=10&strategy=CONFIG'
     assert url == base_address + expected
@@ -397,67 +439,54 @@ def test_fire_message(ctx):
     # result depends on dict contents so ordering is unreliable
     url = ctx.http_context.response['headers']['Location']
     base_address = 'http://10.0.0.1:7777/index.html?'
-    expected = 'gravity=warning&ident=10.0.0.4&message=not%20as%20expected&period=5&strategy=CONFIG'
+    expected = 'gravity=warning&ident=10.0.0.4&message=not%20as%20expected&period=5.0&strategy=CONFIG'
     assert url == base_address + expected
 
 
-def test_get_nbcores(ctx):
+def test_get_nb_cores(ctx):
     """ Test the ViewContext.get_nb_cores method. """
     # test default
-    assert ctx.get_nbcores() == 0
+    assert ctx.get_nb_cores() == 0
     # mock the structure
-    stats = ctx.http_context.supervisord.supvisors.statistician
-    stats.nbcores[ctx.local_identifier] = 4
+    stats = ctx.http_context.supervisord.supvisors.host_compiler
+    stats.nb_cores[ctx.local_identifier] = 4
     # test new call
-    assert ctx.get_nbcores() == 4
+    assert ctx.get_nb_cores() == 4
     # test with unknown address
-    assert ctx.get_nbcores('10.0.0.1') == 0
+    assert ctx.get_nb_cores('10.0.0.1') == 0
     # test with known address
-    stats.nbcores['10.0.0.1'] = 8
-    assert ctx.get_nbcores('10.0.0.1') == 8
+    stats.nb_cores['10.0.0.1'] = 8
+    assert ctx.get_nb_cores('10.0.0.1') == 8
 
 
-def test_get_node_stats(ctx):
+def test_get_node_stats(supvisors, ctx):
     """ Test the ViewContext.get_instance_stats method. """
-    # test default
-    assert ctx.get_instance_stats() is None
-    # add statistics data
-    stats_data = ctx.http_context.supervisord.supvisors.statistician.data
-    stats_data[ctx.local_identifier] = {5: 'data for period 5 at self',
-                                        8: 'data for period 8 at self'}
-    stats_data['10.0.0.1'] = {5: 'data for period 5 at 10.0.0.1',
-                              10: 'data for period 10 at 10.0.0.1'}
     # test with default address
-    assert ctx.get_instance_stats() == 'data for period 5 at self'
-    # test with unknown address parameter
-    assert ctx.get_instance_stats('10.0.0.2') is None
+    instance_stats = ctx.get_instance_stats()
+    assert instance_stats.identifier == supvisors.supvisors_mapper.local_identifier
+    assert instance_stats.period == 5.0
+    # test with unknown identifier
+    assert ctx.get_instance_stats('10.0.0.0') is None
     # test with known address parameter and existing period
-    assert ctx.get_instance_stats('10.0.0.1') == 'data for period 5 at 10.0.0.1'
-    # update period
+    instance_stats = ctx.get_instance_stats('10.0.0.1')
+    assert instance_stats.identifier == '10.0.0.1'
+    assert instance_stats.period == 5.0
+    # update with unknown period
     ctx.parameters[PERIOD] = 8
-    # test with default address and existing period
-    assert ctx.get_instance_stats() == 'data for period 8 at self'
-    # test with known address parameter but missing period
     assert ctx.get_instance_stats('10.0.0.1') is None
 
 
-def test_get_process_stats(mocker, ctx):
+def test_get_process_stats(mocker, supvisors, ctx):
     """ Test the ViewContext.get_process_stats method. """
-    mocked_core = mocker.patch('supvisors.viewcontext.ViewContext.get_nbcores', return_value=4)
-    # reset mocks that have been called in constructor
-    mocked_core.reset_mock()
-    # patch get_address_stats so that it returns no result
-    with patch.object(ctx, 'get_instance_stats', return_value=None) as mocked_stats:
-        assert ctx.get_process_stats('dummy_proc') == (4, None)
-        assert mocked_stats.call_args_list == [call(ctx.local_identifier)]
+    mocked_core = mocker.patch('supvisors.viewcontext.ViewContext.get_nb_cores', return_value=4)
+    # test no result as no data stored
+    assert ctx.get_process_stats('dummy_proc') == (4, None)
     mocked_core.reset_mock()
-    # patch get_address_stats
-    mocked_find = Mock(**{'find_process_stats.return_value': 'mock stats'})
-    with patch.object(ctx, 'get_instance_stats', return_value=mocked_find) as mocked_stats:
-        assert ctx.get_process_stats('dummy_proc', '10.0.0.1') == (4, 'mock stats')
-        assert mocked_stats.call_args_list == [call('10.0.0.1')]
-        assert mocked_core.call_args_list == [call('10.0.0.1')]
-        assert mocked_find.find_process_stats.call_args_list == [call('dummy_proc')]
+    # fill some internal structures
+    mocked_stats = mocker.patch.object(supvisors.process_compiler, 'get_stats', return_value='mock stats')
+    assert ctx.get_process_stats('dummy_proc', '10.0.0.1') == (4, 'mock stats')
+    assert mocked_core.call_args_list == [call('10.0.0.1')]
+    assert mocked_stats.call_args_list == [call('dummy_proc', '10.0.0.1', 5.0)]
 
 
 def test_get_process_status(mocker, ctx):
diff --git a/supvisors/tests/test_viewhandler.py b/supvisors/tests/test_viewhandler.py
index ff62ab4c..9d12810e 100644
--- a/supvisors/tests/test_viewhandler.py
+++ b/supvisors/tests/test_viewhandler.py
@@ -29,7 +29,7 @@
 from supvisors.viewcontext import AUTO, PERIOD, PROCESS, ViewContext
 from supvisors.viewhandler import ViewHandler
 from supvisors.viewimage import process_cpu_img, process_mem_img
-from supvisors.webutils import SUPVISORS_PAGE, MASTER_SYMBOL
+from supvisors.webutils import MASTER_SYMBOL
 from .base import DummyHttpContext
 from .conftest import create_element, create_application
 
@@ -63,7 +63,8 @@ def test_init(http_context, handler):
     assert handler.supvisors is http_context.supervisord.supvisors
     assert handler.sup_ctx is http_context.supervisord.supvisors.context
     assert handler.local_identifier == handler.supvisors.supvisors_mapper.local_identifier
-    assert handler.has_statistics
+    assert handler.has_host_statistics
+    assert handler.has_process_statistics
     assert handler.view_ctx is None
 
 
@@ -145,42 +146,36 @@ def test_write_common(mocker, handler):
                                                         'get_gravity.return_value': 'severe',
                                                         'get_message.return_value': 'a message'})
     # build xml template
-    mocked_meta = Mock(attrib={})
-    mocked_supv = Mock(attrib={})
-    mocked_version = Mock()
-    mocked_identifier = Mock()
-    mocked_refresh = Mock()
-    mocked_autorefresh = Mock(attrib={'class': 'button'})
-    mocked_root = Mock(**{'findmeld.side_effect': [mocked_supv, mocked_version, mocked_identifier, mocked_refresh,
-                                                   mocked_autorefresh]})
+    mocked_meta = create_element()
+    mocked_supv = create_element()
+    mocked_version = create_element()
+    mocked_identifier = create_element()
+    mocked_refresh = create_element()
+    mocked_autorefresh = create_element()
+    mocked_autorefresh.attrib['class'] = 'button'
+    mocked_root = create_element({'meta_mid': mocked_meta, 'supvisors_mid': mocked_supv,
+                                  'version_mid': mocked_version, 'identifier_mid': mocked_identifier,
+                                  'refresh_a_mid': mocked_refresh, 'autorefresh_a_mid': mocked_autorefresh})
     # 1. test no conflict and auto-refresh
     handler.supvisors.fsm.state = SupvisorsStates.OPERATION
     handler.write_common(mocked_root)
-    assert mocked_root.findmeld.call_args_list == [call('supvisors_mid'), call('version_mid'),
-                                                   call('identifier_mid'), call('refresh_a_mid'),
-                                                   call('autorefresh_a_mid')]
+    assert mocked_root.findmeld.call_args_list == [call('version_mid'), call('identifier_mid'),
+                                                   call('refresh_a_mid'), call('autorefresh_a_mid')]
     assert not mocked_meta.deparent.called
-    assert mocked_supv.attributes.call_args_list == [call(href='an url')]
-    assert 'class' not in mocked_supv.attrib
+    assert 'failure' not in mocked_supv.attrib['class']
     assert mocked_version.content.call_args_list == [call(API_VERSION)]
     assert mocked_identifier.content.call_args_list == [call(handler.local_identifier)]
     assert mocked_refresh.attributes.call_args_list == [call(href='an url')]
     assert mocked_autorefresh.attributes.call_args_list == [call(href='an url')]
     assert mocked_autorefresh.attrib['class'] == 'button active'
-    assert handler.view_ctx.format_url.call_args_list == [call('', SUPVISORS_PAGE),
-                                                          call('', 'dummy.html'),
+    assert handler.view_ctx.format_url.call_args_list == [call('', 'dummy.html'),
                                                           call('', 'dummy.html', auto=False)]
     assert mocked_msg.call_args_list == [call(mocked_root, 'severe', 'a message', handler.current_time)]
     # reset mocks
-    mocked_root.findmeld.reset_mock()
-    handler.view_ctx.format_url.reset_mock()
-    mocked_supv.attributes.reset_mock()
-    mocked_version.content.reset_mock()
-    mocked_identifier.content.reset_mock()
-    mocked_refresh.attributes.reset_mock()
-    mocked_autorefresh.attributes.reset_mock()
+    mocked_root.reset_all()
     mocked_autorefresh.attrib['class'] = 'button'
     mocked_msg.reset_mock()
+    handler.view_ctx.format_url.reset_mock()
     # 2. test conflicts and no auto-refresh
     mocked_root.findmeld.side_effect = [mocked_meta, mocked_supv, mocked_version, mocked_identifier, mocked_refresh,
                                         mocked_autorefresh]
@@ -192,15 +187,13 @@ def test_write_common(mocker, handler):
                                                    call('identifier_mid'), call('refresh_a_mid'),
                                                    call('autorefresh_a_mid')]
     assert mocked_meta.deparent.called
-    assert mocked_supv.attributes.call_args_list == [call(href='an url')]
-    assert mocked_supv.attrib == {'class': 'blink'}
+    assert mocked_supv.attrib == {'class': 'failure'}
     assert mocked_version.content.call_args_list == [call(API_VERSION)]
     assert mocked_identifier.content.call_args_list == [call(handler.local_identifier)]
     assert mocked_refresh.attributes.call_args_list == [call(href='an url')]
     assert mocked_autorefresh.attributes.call_args_list == [call(href='an url')]
     assert mocked_autorefresh.attrib['class'] == 'button'
-    assert handler.view_ctx.format_url.call_args_list == [call('', SUPVISORS_PAGE),
-                                                          call('', 'dummy.html'),
+    assert handler.view_ctx.format_url.call_args_list == [call('', 'dummy.html'),
                                                           call('', 'dummy.html', auto=True)]
     assert mocked_msg.call_args_list == [call(mocked_root, 'severe', 'a message', handler.current_time)]
 
@@ -390,19 +383,26 @@ def test_write_nav_applications_operation(handler):
 
 
 def test_write_header(handler):
-    """ Test the write_header method. """
+    """ Test the ViewHandler.write_header method. """
     with pytest.raises(NotImplementedError):
         handler.write_header(Mock())
 
 
-def test_write_periods(handler):
-    """ Test the write_periods method. """
-    # test call with period selection identical to parameter
+def test_write_periods(mocker, handler):
+    """ Test the ViewHandler.write_periods method. """
+    mocked_period = mocker.patch.object(handler, 'write_periods_availability')
+    mocked_root = Mock()
+    handler.write_periods(mocked_root)
+    assert mocked_period.call_args_list == [call(mocked_root, True)]
+
+
+def test_write_periods_availability(handler):
+    """ Test the ViewHandler.write_periods_availability method. """
+    # 1. test call with period selection identical to parameter
     mocked_mid = Mock()
     mocked_root = Mock(**{'findmeld.return_value': mocked_mid})
     # test call when statistics are disabled
-    handler.has_statistics = False
-    handler.write_periods(mocked_root)
+    handler.write_periods_availability(mocked_root, False)
     assert mocked_root.findmeld.call_args_list == [call('period_div_mid')]
     assert mocked_mid.replace.call_args_list == [call('')]
     # patch the meld elements
@@ -411,9 +411,8 @@ def test_write_periods(handler):
     mocked_mid = Mock(**{'repeat.return_value': [(period_elt, 5)]})
     mocked_root = Mock(**{'findmeld.return_value': mocked_mid})
     # test call with period selection identical to parameter
-    handler.has_statistics = True
     handler.view_ctx = Mock(parameters={PERIOD: 5}, **{'format_url.return_value': 'an url'})
-    handler.write_periods(mocked_root)
+    handler.write_periods_availability(mocked_root, True)
     assert mocked_root.findmeld.call_args_list == [call('period_li_mid')]
     assert mocked_mid.repeat.call_args_list == [call(handler.supvisors.options.stats_periods)]
     assert period_elt.findmeld.call_args_list == [call('period_a_mid')]
@@ -426,9 +425,9 @@ def test_write_periods(handler):
     period_elt.findmeld.reset_mock()
     href_elt.content.reset_mock()
     href_elt.attrib['class'] = ''
-    # test call with period selection different from parameter
+    # 2. test call with period selection different from parameter
     handler.view_ctx.parameters[PERIOD] = 10
-    handler.write_periods(mocked_root)
+    handler.write_periods_availability(mocked_root, True)
     assert mocked_root.findmeld.call_args_list == [call('period_li_mid')]
     assert mocked_mid.repeat.call_args_list == [call(handler.supvisors.options.stats_periods)]
     assert period_elt.findmeld.call_args_list == [call('period_a_mid')]
@@ -463,7 +462,7 @@ def test_write_common_process_cpu(handler):
     tr_elt.findmeld.reset_mock()
     cell_elt.replace.reset_mock()
     # test with empty stats
-    info = {'proc_stats': [[]]}
+    info = {'proc_stats': Mock(cpu=[])}
     handler.write_common_process_cpu(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pcpu_a_mid')]
     assert not cell_elt.deparent.called
@@ -474,7 +473,7 @@ def test_write_common_process_cpu(handler):
     cell_elt.replace.reset_mock()
     # test with filled stats on selected process, irix mode
     handler.supvisors.options.stats_irix_mode = True
-    info = {'namespec': 'dummy_proc', 'identifier': '10.0.0.1', 'proc_stats': [[10, 20]], 'nb_cores': 2}
+    info = {'namespec': 'dummy_proc', 'identifier': '10.0.0.1', 'proc_stats': Mock(cpu=[10, 20]), 'nb_cores': 2}
     handler.write_common_process_cpu(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pcpu_a_mid')]
     assert not cell_elt.deparent.called
@@ -490,7 +489,7 @@ def test_write_common_process_cpu(handler):
     del cell_elt.attrib['class']
     # test with filled stats on not selected process, solaris mode
     handler.supvisors.options.stats_irix_mode = False
-    info = {'namespec': 'dummy', 'identifier': '10.0.0.1', 'proc_stats': [[10, 20, 30]], 'nb_cores': 2}
+    info = {'namespec': 'dummy', 'identifier': '10.0.0.1', 'proc_stats': Mock(cpu=[10, 20, 30]), 'nb_cores': 2}
     handler.write_common_process_cpu(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pcpu_a_mid')]
     assert not cell_elt.deparent.called
@@ -507,7 +506,7 @@ def test_write_common_process_cpu(handler):
     del cell_elt.attrib['class']
     # test with filled stats on application (so non process), solaris mode
     handler.supvisors.options.stats_irix_mode = False
-    info = {'namespec': None, 'ident': '10.0.0.1', 'proc_stats': [[10, 20, 30]], 'nb_cores': 2}
+    info = {'namespec': None, 'ident': '10.0.0.1', 'proc_stats': Mock(cpu=[10, 20, 30]), 'nb_cores': 2}
     handler.write_common_process_cpu(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pcpu_a_mid')]
     assert not cell_elt.deparent.called
@@ -520,7 +519,7 @@ def test_write_common_process_cpu(handler):
     tr_elt.findmeld.reset_mock()
     cell_elt.replace.reset_mock()
     # test with statistics disabled
-    handler.has_statistics = False
+    handler.has_process_statistics = False
     handler.write_common_process_cpu(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pcpu_td_mid')]
     assert cell_elt.deparent.call_args_list == [call()]
@@ -538,7 +537,7 @@ def test_write_common_process_mem(handler):
     cell_elt = Mock(attrib={'class': ''})
     tr_elt = Mock(attrib={}, **{'findmeld.return_value': cell_elt})
     # test with no stats
-    info = {'proc_stats': []}
+    info = {'proc_stats': None}
     handler.write_common_process_mem(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pmem_a_mid')]
     assert not cell_elt.deparent.called
@@ -549,7 +548,7 @@ def test_write_common_process_mem(handler):
     tr_elt.findmeld.reset_mock()
     cell_elt.replace.reset_mock()
     # test with empty stats
-    info = {'proc_stats': ([], [])}
+    info = {'proc_stats': Mock(mem=[])}
     handler.write_common_process_mem(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pmem_a_mid')]
     assert not cell_elt.deparent.called
@@ -560,7 +559,7 @@ def test_write_common_process_mem(handler):
     tr_elt.findmeld.reset_mock()
     cell_elt.replace.reset_mock()
     # test with filled stats on selected process
-    info = {'namespec': 'dummy_proc', 'identifier': '10.0.0.2', 'proc_stats': ([], [10, 20])}
+    info = {'namespec': 'dummy_proc', 'identifier': '10.0.0.2', 'proc_stats': Mock(mem=[10, 20])}
     handler.write_common_process_mem(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pmem_a_mid')]
     assert not cell_elt.deparent.called
@@ -575,7 +574,7 @@ def test_write_common_process_mem(handler):
     cell_elt.attributes.reset_mock()
     del cell_elt.attrib['class']
     # test with filled stats on not selected process
-    info = {'namespec': 'dummy', 'identifier': '10.0.0.2', 'proc_stats': ([], [10, 20, 30])}
+    info = {'namespec': 'dummy', 'identifier': '10.0.0.2', 'proc_stats': Mock(mem=[10, 20, 30])}
     handler.write_common_process_mem(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pmem_a_mid')]
     assert not cell_elt.deparent.called
@@ -592,7 +591,7 @@ def test_write_common_process_mem(handler):
     del cell_elt.attrib['class']
     # test with filled stats on application (so non process), solaris mode
     handler.supvisors.options.stats_irix_mode = False
-    info = {'namespec': None, 'identifier': '10.0.0.2', 'proc_stats': ([], [10, 20, 30])}
+    info = {'namespec': None, 'identifier': '10.0.0.2', 'proc_stats': Mock(mem=[10, 20, 30])}
     handler.write_common_process_mem(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pmem_a_mid')]
     assert not cell_elt.deparent.called
@@ -605,7 +604,7 @@ def test_write_common_process_mem(handler):
     tr_elt.findmeld.reset_mock()
     cell_elt.replace.reset_mock()
     # test with statistics disabled
-    handler.has_statistics = False
+    handler.has_process_statistics = False
     handler.write_common_process_mem(tr_elt, info)
     assert tr_elt.findmeld.call_args_list == [call('pmem_td_mid')]
     assert cell_elt.deparent.call_args_list == [call()]
@@ -745,7 +744,7 @@ def test_write_common_process_table(handler):
                'mem_foot_th_mid': mem_foot_elt, 'cpu_foot_th_mid': cpu_foot_elt, 'total_mid': None}
     root = Mock(attrib={}, **{'findmeld.side_effect': lambda x: mid_map[x]})
     # test with statistics enabled
-    handler.has_statistics = True
+    handler.has_process_statistics = True
     handler.write_common_process_table(root)
     assert not root.findmeld.called
     assert not mem_head_elt.deparent.called
@@ -753,7 +752,7 @@ def test_write_common_process_table(handler):
     assert not cpu_head_elt.deparent.called
     assert not cpu_foot_elt.deparent.called
     # test with statistics enabled
-    handler.has_statistics = False
+    handler.has_process_statistics = False
     handler.write_common_process_table(root)
     assert root.findmeld.call_args_list == [call('mem_head_th_mid'), call('cpu_head_th_mid'),
                                             call('mem_foot_th_mid'), call('cpu_foot_th_mid'), call('total_mid')]
@@ -887,10 +886,10 @@ def test_write_detailed_process_cpu(handler):
     avg_elt, slope_elt, dev_elt = Mock(), Mock(), Mock()
     stats_elt = Mock(**{'findmeld.side_effect': [val_elt, avg_elt, slope_elt, dev_elt] * 2})
     # create fake stats
-    proc_stats = ([10, 16, 13],)
+    proc_stats = Mock(times=[1, 2, 3], cpu=[10, 16, 13])
     # test call with empty stats
-    assert not handler.write_detailed_process_cpu(stats_elt, [], 4)
-    assert not handler.write_detailed_process_cpu(stats_elt, ([], []), 4)
+    assert not handler.write_detailed_process_cpu(stats_elt, None, 4)
+    assert not handler.write_detailed_process_cpu(stats_elt, Mock(cpu=[]), 4)
     # test call with irix mode
     handler.supvisors.options.stats_irix_mode = True
     assert handler.write_detailed_process_cpu(stats_elt, proc_stats, 4)
@@ -905,12 +904,12 @@ def test_write_detailed_process_cpu(handler):
     dev_elt.content.reset_mock()
     del val_elt.attrib['class']
     # test call with solaris mode
-    proc_stats = ([10, 16, 24],)
+    proc_stats = Mock(times=[1, 2, 3], cpu=[10, 16, 24])
     handler.supvisors.options.stats_irix_mode = False
     assert handler.write_detailed_process_cpu(stats_elt, proc_stats, 4)
     assert val_elt.attrib['class'] == 'increase'
     assert val_elt.content.call_args_list == [call('6.00%')]
-    assert avg_elt.content.call_args_list == [call('16.67%')]
+    assert avg_elt.content.call_args_list == [call('4.17%')]
     assert slope_elt.content.call_args_list == [call('7.00')]
     assert dev_elt.content.call_args_list == [call('5.73')]
 
@@ -922,10 +921,10 @@ def test_write_detailed_process_mem(handler):
     avg_elt, slope_elt, dev_elt = Mock(), Mock(), Mock()
     stats_elt = Mock(**{'findmeld.side_effect': [val_elt, avg_elt, slope_elt, dev_elt] * 2})
     # create fake stats
-    proc_stats = ((), [20, 32, 32])
+    proc_stats = Mock(times=[1, 2, 3], mem=[20, 32, 32])
     # test call with empty stats
-    assert not handler.write_detailed_process_mem(stats_elt, [], )
-    assert not handler.write_detailed_process_mem(stats_elt, ([], []))
+    assert not handler.write_detailed_process_mem(stats_elt, None)
+    assert not handler.write_detailed_process_mem(stats_elt, Mock(mem=[]))
     # test call with irix mode
     handler.supvisors.options.stats_irix_mode = True
     assert handler.write_detailed_process_mem(stats_elt, proc_stats)
@@ -952,17 +951,20 @@ def test_write_process_plots(mocker, handler):
     pytest.importorskip('matplotlib', reason='cannot test as optional matplotlib is not installed')
     # get patches
     mocked_export = mocker.patch('supvisors.plot.StatisticsPlot.export_image')
-    mocked_add = mocker.patch('supvisors.plot.StatisticsPlot.add_plot')
+    mocked_time = mocker.patch('supvisors.plot.StatisticsPlot.add_timeline')
+    mocked_plot = mocker.patch('supvisors.plot.StatisticsPlot.add_plot')
     # test call with dummy stats and Solaris mode
-    proc_stats = ([10, 16, 24], [20, 32, 32])
+    proc_stats = Mock(times=[1, 2, 3], cpu=[10, 16, 24], mem=[20, 32, 32])
     handler.write_process_plots(proc_stats, 2)
-    assert mocked_add.call_args_list == [call('CPU', '%', [5, 8, 12]), call('MEM', '%', [20, 32, 32])]
+    assert mocked_time.call_args_list == [call([1, 2, 3]), call([1, 2, 3])]
+    assert mocked_plot.call_args_list == [call('CPU', '%', [5, 8, 12]), call('MEM', '%', [20, 32, 32])]
     assert mocked_export.call_args_list == [call(process_cpu_img), call(process_mem_img)]
     mocker.resetall()
     # test call with dummy stats and IRIX mode
     handler.supvisors.options.stats_irix_mode = True
     handler.write_process_plots(proc_stats, 2)
-    assert mocked_add.call_args_list == [call('CPU', '%', [10, 16, 24]), call('MEM', '%', [20, 32, 32])]
+    assert mocked_time.call_args_list == [call([1, 2, 3]), call([1, 2, 3])]
+    assert mocked_plot.call_args_list == [call('CPU', '%', [10, 16, 24]), call('MEM', '%', [20, 32, 32])]
     assert mocked_export.call_args_list == [call(process_cpu_img), call(process_mem_img)]
 
 
diff --git a/supvisors/tests/test_viewhostinstance.py b/supvisors/tests/test_viewhostinstance.py
index db10aa7f..944bd0a8 100644
--- a/supvisors/tests/test_viewhostinstance.py
+++ b/supvisors/tests/test_viewhostinstance.py
@@ -17,18 +17,16 @@
 # limitations under the License.
 # ======================================================================
 
-import pytest
+from unittest.mock import call, Mock
 
+import pytest
 from supervisor.web import StatusView
 
-from unittest.mock import call, Mock
-
 from supvisors.viewcontext import CPU, INTF, ViewContext
 from supvisors.viewhandler import ViewHandler
 from supvisors.viewhostinstance import HostInstanceView
 from supvisors.viewimage import host_cpu_img, host_io_img, host_mem_img
 from supvisors.webutils import HOST_INSTANCE_PAGE
-
 from .base import DummyHttpContext
 
 
@@ -64,15 +62,15 @@ def test_write_contents_no_plot(mocker, view):
     # force import error on SupvisorsPlot
     mocker.patch.dict('sys.modules', {'supvisors.plot': None})
     # set context (meant to be set through render)
-    dummy_stats = Mock(cpu='cpu', mem='mem', io='io')
+    dummy_stats = Mock(cpu='cpu', mem='mem', io='io', times='times')
     view.view_ctx = Mock(**{'get_instance_stats.return_value': dummy_stats})
     # replace root structure
     mocked_root = Mock()
     # test call
     view.write_contents(mocked_root)
-    assert mocked_processor.call_args_list == [call(mocked_root, 'cpu')]
-    assert mocked_memory.call_args_list == [call(mocked_root, 'mem')]
-    assert mocked_network.call_args_list == [call(mocked_root, 'io')]
+    assert mocked_processor.call_args_list == [call(mocked_root, 'cpu', 'times')]
+    assert mocked_memory.call_args_list == [call(mocked_root, 'mem', 'times')]
+    assert mocked_network.call_args_list == [call(mocked_root, 'io', 'times')]
     assert not mocked_export.called
 
 
@@ -87,18 +85,18 @@ def test_write_contents(mocker, view):
     mocked_mem = mocker.patch.object(view, '_write_mem_image')
     mocked_cpu = mocker.patch.object(view, '_write_cpu_image')
     # set context (meant to be set through render)
-    dummy_stats = Mock(cpu='cpu', mem='mem', io='io')
+    dummy_stats = Mock(cpu='cpu', mem='mem', io='io', times='times')
     view.view_ctx = Mock(**{'get_instance_stats.return_value': dummy_stats})
     # replace root structure
     mocked_root = Mock()
     # test call
     view.write_contents(mocked_root)
-    assert mocked_processor.call_args_list == [call(mocked_root, 'cpu')]
-    assert mocked_memory.call_args_list == [call(mocked_root, 'mem')]
-    assert mocked_network.call_args_list == [call(mocked_root, 'io')]
-    assert mocked_cpu.call_args_list == [call('cpu')]
-    assert mocked_mem.call_args_list == [call('mem')]
-    assert mocked_io.call_args_list == [call('io')]
+    assert mocked_processor.call_args_list == [call(mocked_root, 'cpu', 'times')]
+    assert mocked_memory.call_args_list == [call(mocked_root, 'mem', 'times')]
+    assert mocked_network.call_args_list == [call(mocked_root, 'io', 'times')]
+    assert mocked_cpu.call_args_list == [call('cpu', 'times')]
+    assert mocked_mem.call_args_list == [call('mem', 'times')]
+    assert mocked_io.call_args_list == [call('io', 'times')]
 
 
 def test_write_processor_single_title(view):
@@ -130,8 +128,9 @@ def test_write_processor_single_statistics(mocker, view):
     # replace root element
     mocked_root = Mock()
     # test method call
-    view._write_processor_single_statistics(mocked_root, [1.523, 2.456])
-    assert mocked_common.call_args_list == [call(mocked_root, [1.523, 2.456], 'cpuval_td_mid', 'cpuavg_td_mid',
+    view._write_processor_single_statistics(mocked_root, [1.523, 2.456], [1, 2, 3])
+    assert mocked_common.call_args_list == [call(mocked_root, [1.523, 2.456], [1, 2, 3],
+                                                 'cpuval_td_mid', 'cpuavg_td_mid',
                                                  'cpuslope_td_mid', 'cpudev_td_mid')]
 
 
@@ -146,9 +145,10 @@ def test_write_processor_statistics(mocker, view):
     mocked_mid = Mock(**{'repeat.return_value': [(mocked_trs[0], 'cpu stats 0'), (mocked_trs[1], 'cpu stats 1')]})
     mocked_root = Mock(**{'findmeld.return_value': mocked_mid})
     # test call
-    view.write_processor_statistics(mocked_root, [])
+    view.write_processor_statistics(mocked_root, [], [1, 2, 3])
     assert mocked_title.call_args_list == [call(mocked_trs[0], 1, 0), call(mocked_trs[1], 1, 1)]
-    assert mocked_stats.call_args_list == [call(mocked_trs[0], 'cpu stats 0'), call(mocked_trs[1], 'cpu stats 1')]
+    assert mocked_stats.call_args_list == [call(mocked_trs[0], 'cpu stats 0', [1, 2, 3]),
+                                           call(mocked_trs[1], 'cpu stats 1', [1, 2, 3])]
     assert mocked_trs[0].attrib == {'class': 'brightened'}
     assert mocked_trs[1].attrib == {'class': 'shaded'}
 
@@ -159,8 +159,9 @@ def test_write_memory_statistics(mocker, view):
     # replace root element
     mocked_root = Mock()
     # test method call
-    view.write_memory_statistics(mocked_root, [1.523, 2.456])
-    assert mocked_common.call_args_list == [call(mocked_root, [1.523, 2.456], 'memval_td_mid', 'memavg_td_mid',
+    view.write_memory_statistics(mocked_root, [1.523, 2.456], [1, 2, 3])
+    assert mocked_common.call_args_list == [call(mocked_root, [1.523, 2.456], [1, 2, 3],
+                                                 'memval_td_mid', 'memavg_td_mid',
                                                  'memslope_td_mid', 'memdev_td_mid')]
 
 
@@ -211,19 +212,21 @@ def test_write_network_single_statistics(mocker, view):
     mocked_title_mid = Mock()
     mocked_tr = Mock(**{'findmeld.return_value': mocked_title_mid})
     # in first call, test no rate, slope and standard deviation
-    view._write_network_single_statistics(mocked_tr, [1.523, 2.456], False)
+    view._write_network_single_statistics(mocked_tr, [1.523, 2.456], [1, 2, 3], False)
     assert mocked_tr.findmeld.call_args_list == [call('intfrxtx_td_mid')]
     assert mocked_title_mid.content.call_args_list == [call('Tx')]
-    assert mocked_common.call_args_list == [call(mocked_tr, [1.523, 2.456], 'intfval_td_mid', 'intfavg_td_mid',
+    assert mocked_common.call_args_list == [call(mocked_tr, [1.523, 2.456], [1, 2, 3],
+                                                 'intfval_td_mid', 'intfavg_td_mid',
                                                  'intfslope_td_mid', 'intfdev_td_mid')]
     mocked_tr.reset_mock()
     mocked_title_mid.content.reset_mock()
     mocked_common.reset_mock()
     # in second call, test no rate, slope and standard deviation
-    view._write_network_single_statistics(mocked_tr, [1.523, 2.456], True)
+    view._write_network_single_statistics(mocked_tr, [1.523, 2.456], [1, 2, 3], True)
     assert mocked_tr.findmeld.call_args_list == [call('intfrxtx_td_mid')]
     assert mocked_title_mid.content.call_args_list == [call('Rx')]
-    assert mocked_common.call_args_list == [call(mocked_tr, [1.523, 2.456], 'intfval_td_mid', 'intfavg_td_mid',
+    assert mocked_common.call_args_list == [call(mocked_tr, [1.523, 2.456], [1, 2, 3],
+                                                 'intfval_td_mid', 'intfavg_td_mid',
                                                  'intfslope_td_mid', 'intfdev_td_mid')]
 
 
@@ -241,8 +244,9 @@ def test_write_network_statistics(mocker, view):
                                                  (mocked_trs[3], ('eth0', 'eth0 sent'))]})
     mocked_root = Mock(**{'findmeld.return_value': mocked_mid})
     # test method with dummy stats
-    dummy_stats = {'lo': ['lo recv', 'lo sent'], 'eth0': ['eth0 recv', 'eth0 sent']}
-    view.write_network_statistics(mocked_root, dummy_stats)
+    dummy_io_stats = {'lo': ['lo recv', 'lo sent'], 'eth0': ['eth0 recv', 'eth0 sent']}
+    dummy_times = [1, 2, 3]
+    view.write_network_statistics(mocked_root, dummy_io_stats, dummy_times)
     # check calls
     assert mocked_root.findmeld.call_args_list == [call('intf_tr_mid')]
     assert mocked_mid.repeat.call_args_list == [call([('lo', 'lo recv'), ('lo', 'lo sent'),
@@ -255,10 +259,10 @@ def test_write_network_statistics(mocker, view):
                                            call(mocked_trs[1], 'eth0', 'lo', False, False),
                                            call(mocked_trs[2], 'eth0', 'eth0', True, True),
                                            call(mocked_trs[3], 'eth0', 'eth0', False, True)]
-    assert mocked_stats.call_args_list == [call(mocked_trs[0], 'lo recv', True),
-                                           call(mocked_trs[1], 'lo sent', False),
-                                           call(mocked_trs[2], 'eth0 recv', True),
-                                           call(mocked_trs[3], 'eth0 sent', False)]
+    assert mocked_stats.call_args_list == [call(mocked_trs[0], 'lo recv', dummy_times, True),
+                                           call(mocked_trs[1], 'lo sent', dummy_times, False),
+                                           call(mocked_trs[2], 'eth0 recv', dummy_times, True),
+                                           call(mocked_trs[3], 'eth0 sent', dummy_times, False)]
 
 
 def test_write_common_statistics(mocker, view):
@@ -275,7 +279,7 @@ def test_write_common_statistics(mocker, view):
                                                  mocked_val_mid, mocked_avg_mid,
                                                  mocked_slope_mid, mocked_dev_mid]})
     # in first call, test empty stats
-    view._write_common_statistics(mocked_tr, [], 'val_mid', 'avg_mid', 'slope_mid', 'dev_mid')
+    view._write_common_statistics(mocked_tr, [], [], 'val_mid', 'avg_mid', 'slope_mid', 'dev_mid')
     assert not mocked_tr.findmeld.called
     assert not mocked_stats.called
     assert not mocked_class.called
@@ -284,9 +288,9 @@ def test_write_common_statistics(mocker, view):
     assert not mocked_slope_mid.called
     assert not mocked_dev_mid.called
     # in second call, test no rate, slope and standard deviation
-    view._write_common_statistics(mocked_tr, [1.523, 2.456], 'val_mid', 'avg_mid', 'slope_mid', 'dev_mid')
+    view._write_common_statistics(mocked_tr, [1.523, 2.456], [1, 2], 'val_mid', 'avg_mid', 'slope_mid', 'dev_mid')
     assert mocked_tr.findmeld.call_args_list == [call('val_mid'), call('avg_mid')]
-    assert mocked_stats.call_args_list == [call([1.523, 2.456])]
+    assert mocked_stats.call_args_list == [call([1, 2], [1.523, 2.456])]
     assert not mocked_class.called
     assert mocked_val_mid.content.call_args_list == [call('2.46')]
     assert mocked_avg_mid.content.call_args_list == [call('10.23')]
@@ -296,8 +300,8 @@ def test_write_common_statistics(mocker, view):
     mocked_val_mid.content.reset_mock()
     mocked_avg_mid.content.reset_mock()
     # in third call, test no rate, slope and standard deviation
-    view._write_common_statistics(mocked_tr, [1.523, 2.456], 'val_mid', 'avg_mid', 'slope_mid', 'dev_mid')
-    assert mocked_stats.call_args_list == [call([1.523, 2.456])]
+    view._write_common_statistics(mocked_tr, [1.523, 2.456], [1, 2], 'val_mid', 'avg_mid', 'slope_mid', 'dev_mid')
+    assert mocked_stats.call_args_list == [call([1, 2], [1.523, 2.456])]
     assert mocked_class.call_args_list == [call(mocked_val_mid, 2)]
     assert mocked_tr.findmeld.call_args_list == [call('val_mid'), call('avg_mid'),
                                                  call('val_mid'), call('avg_mid'), call('slope_mid'), call('dev_mid')]
@@ -310,36 +314,45 @@ def test_write_common_statistics(mocker, view):
 def test_write_cpu_image(mocker, view):
     """ Test the _write_cpu_image method. """
     mocked_export = mocker.patch('supvisors.plot.StatisticsPlot.export_image')
-    mocked_add = mocker.patch('supvisors.plot.StatisticsPlot.add_plot')
+    mocked_plot = mocker.patch('supvisors.plot.StatisticsPlot.add_plot')
+    mocked_time = mocker.patch('supvisors.plot.StatisticsPlot.add_timeline')
     # set context (meant to be set through render)
     view.view_ctx = Mock(parameters={CPU: 0}, **{'cpu_id_to_string.return_value': ViewContext.cpu_id_to_string(0)})
     # just test calls to StatisticsPlot
-    dummy_stats = ['#all stats', '#0 stats', '#1 stats']
-    view._write_cpu_image(dummy_stats)
-    assert mocked_add.call_args_list == [call('CPU #all', '%', '#all stats')]
+    dummy_cpu_stats = ['#all stats', '#0 stats', '#1 stats']
+    dummy_times_stats = [1, 2, 3]
+    view._write_cpu_image(dummy_cpu_stats, dummy_times_stats)
+    assert mocked_time.call_args_list == [call(dummy_times_stats)]
+    assert mocked_plot.call_args_list == [call('CPU #all', '%', '#all stats')]
     assert mocked_export.call_args_list == [call(host_cpu_img)]
 
 
 def test_write_mem_image(mocker, view):
     """ Test the _write_mem_image method. """
     mocked_export = mocker.patch('supvisors.plot.StatisticsPlot.export_image')
-    mocked_add = mocker.patch('supvisors.plot.StatisticsPlot.add_plot')
+    mocked_plot = mocker.patch('supvisors.plot.StatisticsPlot.add_plot')
+    mocked_time = mocker.patch('supvisors.plot.StatisticsPlot.add_timeline')
     # just test calls to StatisticsPlot
-    dummy_stats = ['mem 1', 'mem 2']
-    view._write_mem_image(dummy_stats)
-    assert mocked_add.call_args_list == [call('MEM', '%', dummy_stats)]
+    dummy_mem_stats = ['mem 1', 'mem 2']
+    dummy_times_stats = [1, 2, 3]
+    view._write_mem_image(dummy_mem_stats, dummy_times_stats)
+    assert mocked_time.call_args_list == [call(dummy_times_stats)]
+    assert mocked_plot.call_args_list == [call('MEM', '%', dummy_mem_stats)]
     assert mocked_export.call_args_list == [call(host_mem_img)]
 
 
 def test_write_io_image(mocker, view):
     """ Test the _write_io_image method. """
     mocked_export = mocker.patch('supvisors.plot.StatisticsPlot.export_image')
-    mocked_add = mocker.patch('supvisors.plot.StatisticsPlot.add_plot')
+    mocked_plot = mocker.patch('supvisors.plot.StatisticsPlot.add_plot')
+    mocked_time = mocker.patch('supvisors.plot.StatisticsPlot.add_timeline')
     # set context (meant to be set through render)
     view.view_ctx = Mock(parameters={INTF: 'eth0'})
     # just test calls to StatisticsPlot
-    dummy_stats = {'lo': ['lo recv', 'lo sent'], 'eth0': ['eth0 recv', 'eth0 sent']}
-    view._write_io_image(dummy_stats)
-    assert mocked_add.call_args_list == [call('eth0 recv', 'kbits/s', 'eth0 recv'),
-                                         call('eth0 sent', 'kbits/s', 'eth0 sent')]
+    dummy_io_stats = {'lo': ['lo recv', 'lo sent'], 'eth0': ['eth0 recv', 'eth0 sent']}
+    dummy_times_stats = [1, 2, 3]
+    view._write_io_image(dummy_io_stats, dummy_times_stats)
+    assert mocked_time.call_args_list == [call(dummy_times_stats)]
+    assert mocked_plot.call_args_list == [call('eth0 recv', 'kbits/s', 'eth0 recv'),
+                                          call('eth0 sent', 'kbits/s', 'eth0 sent')]
     assert mocked_export.call_args_list == [call(host_io_img)]
diff --git a/supvisors/tests/test_viewinstance.py b/supvisors/tests/test_viewinstance.py
index d9733227..0c63fd51 100644
--- a/supvisors/tests/test_viewinstance.py
+++ b/supvisors/tests/test_viewinstance.py
@@ -53,7 +53,8 @@ def view_no_stats(http_context):
     # apply the forced inheritance done in supvisors.plugin
     StatusView.__bases__ = (ViewHandler,)
     # create the instance to be tested
-    http_context.supervisord.supvisors.options.stats_enabled = False
+    http_context.supervisord.supvisors.host_collector = None
+    http_context.supervisord.supvisors.process_collector = None
     return SupvisorsInstanceView(http_context, HOST_INSTANCE_PAGE)
 
 
@@ -64,7 +65,8 @@ def test_init(view):
         assert isinstance(view, klass)
     # test parameter page name
     assert view.page_name == HOST_INSTANCE_PAGE
-    assert view.has_statistics
+    assert view.has_host_statistics
+    assert view.has_process_statistics
 
 
 def test_init_no_stats(view_no_stats):
@@ -74,7 +76,8 @@ def test_init_no_stats(view_no_stats):
         assert isinstance(view_no_stats, klass)
     # test parameter page name
     assert view_no_stats.page_name == HOST_INSTANCE_PAGE
-    assert not view_no_stats.has_statistics
+    assert not view_no_stats.has_host_statistics
+    assert not view_no_stats.has_process_statistics
 
 
 def test_render(mocker, view):
@@ -173,10 +176,10 @@ def test_write_instance_actions(mocker, view):
 def test_write_view_switch(view):
     """ Test the SupvisorsInstanceView.write_view_switch method. """
     # set context (meant to be set through constructor and render)
-    view.has_statistics = True
+    view.has_host_statistics = True
     view.view_ctx = Mock(**{'format_url.return_value': 'an url'})
     # set instance context
-    mocked_status = Mock(**{'supvisors_id.host_name': '10.0.0.1'})
+    mocked_status = Mock(**{'supvisors_id.host_id': '10.0.0.1'})
     # build root structure
     mocked_process_view_mid = create_element()
     mocked_host_view_mid = create_element()
@@ -206,7 +209,7 @@ def test_write_view_switch(view):
     mocked_root.reset_all()
     view.view_ctx.format_url.reset_mock()
     # test call when statistics are disabled or collector is missing
-    view.has_statistics = False
+    view.has_host_statistics = False
     for page in [PROC_INSTANCE_PAGE, HOST_INSTANCE_PAGE]:
         view.page_name = page
         view.write_view_switch(mocked_root, mocked_status)
diff --git a/supvisors/tests/test_viewprocinstance.py b/supvisors/tests/test_viewprocinstance.py
index d303ab53..df0c1c22 100644
--- a/supvisors/tests/test_viewprocinstance.py
+++ b/supvisors/tests/test_viewprocinstance.py
@@ -25,7 +25,6 @@
 
 from supvisors.ttypes import ApplicationStates
 from supvisors.viewhandler import ViewHandler
-from supvisors.viewinstance import SupvisorsInstanceView
 from supvisors.viewprocinstance import *
 from supvisors.webutils import PROC_INSTANCE_PAGE
 from .base import DummyHttpContext, ProcessInfoDatabase, process_info_by_name
@@ -59,6 +58,20 @@ def test_init(view):
     assert view.page_name == PROC_INSTANCE_PAGE
 
 
+def test_write_periods(mocker, view):
+    """ Test the ProcInstanceView.write_periods method. """
+    mocked_period = mocker.patch.object(view, 'write_periods_availability')
+    mocked_root = Mock()
+    # test with process statistics to be displayed
+    view.write_periods(mocked_root)
+    assert mocked_period.call_args_list == [call(mocked_root, True)]
+    mocked_period.reset_mock()
+    # test with process statistics NOT to be displayed
+    view.has_process_statistics = False
+    view.write_periods(mocked_root)
+    assert mocked_period.call_args_list == [call(mocked_root, False)]
+
+
 def test_write_contents(mocker, view):
     """ Test the ProcInstanceView.write_contents method. """
     mocked_stats = mocker.patch.object(view, 'write_process_statistics')
@@ -228,32 +241,56 @@ def test_sort_data(mocker, view):
                                {'application_name': 'sample_test_2', 'process_name': 'yeux_01', 'single': False}]
 
 
-def test_get_application_summary(view):
+def test_get_application_summary(mocker, view):
     """ Test the ProcInstanceView.get_application_summary method. """
+    mocked_sum = mocker.patch.object(view, 'sum_process_info')
     # patch the context
     view.view_ctx = Mock(local_identifier='10.0.0.1')
     view.sup_ctx.applications['dummy_appli'] = Mock(state=ApplicationStates.RUNNING,
                                                     **{'get_operational_status.return_value': 'good'})
     # prepare parameters
-    proc_1 = {'statecode': ProcessStates.RUNNING, 'expected_load': 5, 'nb_cores': 8, 'proc_stats': [[10], [5]]}
-    proc_2 = {'statecode': ProcessStates.STARTING, 'expected_load': 15, 'nb_cores': 8, 'proc_stats': [[], []]}
-    proc_3 = {'statecode': ProcessStates.BACKOFF, 'expected_load': 7, 'nb_cores': 8, 'proc_stats': [[8], [22]]}
-    proc_4 = {'statecode': ProcessStates.FATAL, 'expected_load': 25, 'nb_cores': 8, 'proc_stats': None}
+    proc_1, proc_2, proc_3, proc_4 = Mock(), Mock(), Mock(), Mock()
     # test with empty list of processes
     expected = {'application_name': 'dummy_appli', 'process_name': None, 'namespec': None,
                 'disabled': False, 'startable': False,
                 'identifier': '10.0.0.1', 'statename': 'RUNNING', 'statecode': 2, 'gravity': 'RUNNING',
                 'has_crashed': False, 'description': 'good', 'nb_processes': 0,
                 'expected_load': 0, 'nb_cores': 0, 'proc_stats': None}
+    mocked_sum.return_value = 0, 0, None
     assert view.get_application_summary('dummy_appli', []) == expected
     # test with non-running processes
     expected.update({'nb_processes': 1})
     assert view.get_application_summary('dummy_appli', [proc_4]) == expected
     # test with a mix of running and non-running processes
-    expected.update({'nb_processes': 4, 'expected_load': 27, 'nb_cores': 8, 'proc_stats': [[18], [27]]})
+    appli_stats = Mock()
+    mocked_sum.return_value = 27, 8, appli_stats
+    expected.update({'nb_processes': 4, 'expected_load': 27, 'nb_cores': 8, 'proc_stats': appli_stats})
     assert view.get_application_summary('dummy_appli', [proc_1, proc_2, proc_3, proc_4]) == expected
 
 
+def test_sum_process_info():
+    """ Test the ProcInstanceView.sum_process_info method. """
+    # prepare parameters
+    proc_1 = {'statecode': ProcessStates.RUNNING, 'expected_load': 5, 'nb_cores': 8,
+              'proc_stats': Mock(cpu=[10], mem=[5])}
+    proc_2 = {'statecode': ProcessStates.STARTING, 'expected_load': 15, 'nb_cores': 8,
+              'proc_stats': Mock(cpu=[], mem=[])}
+    proc_3 = {'statecode': ProcessStates.BACKOFF, 'expected_load': 7, 'nb_cores': 8,
+              'proc_stats': Mock(cpu=[8], mem=[22])}
+    proc_4 = {'statecode': ProcessStates.FATAL, 'expected_load': 25, 'nb_cores': 8,
+              'proc_stats': None}
+    # test with empty list of processes
+    assert ProcInstanceView.sum_process_info([]) == (0, 0, None)
+    # test with non-running processes
+    assert ProcInstanceView.sum_process_info([proc_4]) == (0, 0, None)
+    # test with a mix of running and non-running processes
+    expected_load, nb_cores, appli_stats = ProcInstanceView.sum_process_info([proc_1, proc_2, proc_3, proc_4])
+    assert expected_load == 27
+    assert nb_cores == 8
+    assert appli_stats.cpu == [18]
+    assert appli_stats.mem == [27]
+
+
 def test_write_process_table(mocker, view):
     """ Test the ProcInstanceView.write_process_table method. """
     mocked_shex = mocker.patch.object(view, 'write_global_shex')
@@ -558,7 +595,7 @@ def test_write_total_status(mocker, view):
     tr_elt.findmeld.reset_mock()
     load_elt.content.reset_mock()
     # test call with process stats and irix mode
-    mocked_sum.return_value = (50, 2, [[12], [25]])
+    mocked_sum.return_value = 50, 2, Mock(cpu=[12], mem=[25])
     view.supvisors.options.stats_irix_mode = True
     view.write_total_status(root_elt, sorted_data, excluded_data)
     assert mocked_sum.call_args_list == [call([1, 2, 3, 4])]
diff --git a/supvisors/tests/test_wssubscriber.py b/supvisors/tests/test_wssubscriber.py
new file mode 100644
index 00000000..99517147
--- /dev/null
+++ b/supvisors/tests/test_wssubscriber.py
@@ -0,0 +1,86 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# ======================================================================
+# Copyright 2022 Julien LE CLEACH
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ======================================================================
+
+import pytest
+pytest.importorskip('websockets', reason='cannot test as optional websockets is not installed')
+
+from unittest.mock import call, Mock
+
+from supvisors.client.wssubscriber import *
+
+
+@pytest.fixture
+def logger():
+    return Mock(spec=Logger)
+
+
+@pytest.fixture
+def interface(logger):
+    intf = SupvisorsWsEventInterface('localhost', 7777, logger)
+    yield intf
+    intf.stop()
+
+
+def test_creation(logger, interface):
+    """ Test the initialization of the SupvisorsZmqEventInterface. """
+    assert isinstance(interface, EventSubscriberInterface)
+    assert isinstance(interface, WsEventSubscriber)
+    assert interface.intf is interface
+    assert interface.node_name == 'localhost'
+    assert interface.event_port == 7777
+    assert interface.logger is logger
+
+
+def test_on_receive(interface, logger):
+    """ Dummy tests about logging when receiving messages from Supvisors. """
+    # test reception of Supvisors status message
+    interface.on_supvisors_status('supvisors payload')
+    assert logger.info.call_args_list == [call('SupvisorsWsEventInterface.on_supvisors_status:'
+                                               ' got Supvisors Status message: supvisors payload')]
+    logger.info.reset_mock()
+    # test reception of Supvisors Instance status message
+    interface.on_instance_status('instance payload')
+    assert logger.info.call_args_list == [call('SupvisorsWsEventInterface.on_instance_status:'
+                                               ' got Instance Status message: instance payload')]
+    logger.info.reset_mock()
+    # test reception of Supvisors Application status message
+    interface.on_application_status('application payload')
+    assert logger.info.call_args_list == [call('SupvisorsWsEventInterface.on_application_status:'
+                                               ' got Application Status message: application payload')]
+    logger.info.reset_mock()
+    # test reception of Supvisors Process event message
+    interface.on_process_event('process event payload')
+    assert logger.info.call_args_list == [call('SupvisorsWsEventInterface.on_process_event:'
+                                               ' got Process Event message: process event payload')]
+    logger.info.reset_mock()
+    # test reception of Supvisors Process status message
+    interface.on_process_status('process payload')
+    assert logger.info.call_args_list == [call('SupvisorsWsEventInterface.on_process_status:'
+                                               ' got Process Status message: process payload')]
+    logger.info.reset_mock()
+    # test reception of Supvisors Application status message
+    interface.on_host_statistics('host statistics payload')
+    assert logger.info.call_args_list == [call('SupvisorsWsEventInterface.on_host_statistics:'
+                                               ' got Host Statistics message: host statistics payload')]
+    logger.info.reset_mock()
+    # test reception of Supvisors Application status message
+    interface.on_process_statistics('process statistics payload')
+    assert logger.info.call_args_list == [call('SupvisorsWsEventInterface.on_process_statistics:'
+                                               ' got Process Statistics message: process statistics payload')]
+    logger.info.reset_mock()
diff --git a/supvisors/tests/test_zmqsubscriber.py b/supvisors/tests/test_zmqsubscriber.py
index d1d99601..b25b3562 100644
--- a/supvisors/tests/test_zmqsubscriber.py
+++ b/supvisors/tests/test_zmqsubscriber.py
@@ -18,220 +18,76 @@
 # ======================================================================
 
 import pytest
+pytest.importorskip('zmq', reason='cannot test as optional pyzmq is not installed')
 
-from supvisors.client.zmqsubscriber import *
 from unittest.mock import call, Mock
-from zmq import ZMQError
-
 
-def test_create_logger(mocker):
-    """ Test the create_logger method. """
-    mocked_stdout = mocker.patch('supervisor.loggers.handle_stdout')
-    mocked_file = mocker.patch('supervisor.loggers.handle_file')
-    # test default
-    default_format = '%(asctime)s;%(levelname)s;%(message)s\n'
-    logger = create_logger()
-    assert isinstance(logger, loggers.Logger)
-    assert logger.level == LevelsByName.INFO
-    assert mocked_stdout.call_args_list == [call(logger, default_format)]
-    assert mocked_file.call_args_list == [call(logger, r'subscriber.log', default_format, True, 10485760, 1)]
-    mocker.resetall()
-    # test with parameters
-    new_format = '%(asctime)s %(message)s'
-    logger = create_logger('/tmp/client.log', LevelsByName.CRIT, new_format, False, 1024, 10, False)
-    assert isinstance(logger, loggers.Logger)
-    assert logger.level == LevelsByName.CRIT
-    assert not mocked_stdout.called
-    assert mocked_file.call_args_list == [call(logger, '/tmp/client.log', new_format, False, 1024, 10)]
+from supvisors.client.zmqsubscriber import *
 
 
 @pytest.fixture
 def context():
-    return zmq.Context.instance()
+    return zmq.asyncio.Context.instance()
 
 
 @pytest.fixture
 def logger():
-    return Mock(spec=loggers.Logger)
+    return Mock(spec=Logger)
 
 
 @pytest.fixture
 def interface(context, logger):
-    intf = SupvisorsZmqEventInterface(context, 7777, logger)
+    intf = SupvisorsZmqEventInterface(context, 'localhost', 7777, logger)
     yield intf
-    intf.subscriber.close()
+    intf.stop()
 
 
 def test_creation(context, logger, interface):
-    """ Test the initialization of the SupvisorsEventInterface. """
-    assert isinstance(interface, threading.Thread)
+    """ Test the initialization of the SupvisorsZmqEventInterface. """
+    assert isinstance(interface, EventSubscriberInterface)
+    assert isinstance(interface, ZmqEventSubscriber)
     assert interface.zmq_context is context
+    assert interface.intf is interface
+    assert interface.node_name == 'localhost'
     assert interface.event_port == 7777
     assert interface.logger is logger
-    assert isinstance(interface.subscriber, EventSubscriber)
-    assert isinstance(interface.stop_event, threading.Event)
-
-
-def test_stop(interface):
-    """ Test the SupvisorsEventInterface.stop method. """
-    assert not interface.stop_event.is_set()
-    interface.stop()
-    assert interface.stop_event.is_set()
-
-
-def test_run_nothing(mocker, context, logger, interface):
-    """ Test the behavior of the thread loop when nothing to poll. """
-    # patch the context
-    mocker.patch.object(interface.stop_event, 'is_set', side_effect=[False, True])
-    logger.reset_mock()
-    # test run method when nothing to poll
-    interface.run()
-    assert logger.info.call_args_list == [call('SupvisorsEventInterface.run: entering main loop')]
-    assert not logger.debug.called
-    assert not logger.error.called
-    assert logger.warn.call_args_list == [call('SupvisorsEventInterface.run: exiting main loop')]
-    assert interface.subscriber.socket.closed
-
-
-def test_run_no_pollin(mocker, context, logger, interface):
-    """ Test the behavior of the thread loop when nothing to poll in. """
-    # patch the context
-    mocker.patch.object(interface.stop_event, 'is_set', side_effect=[False, True])
-    mocker.patch('zmq.Poller.poll', return_value={interface.subscriber.socket: zmq.POLLOUT})
-    logger.reset_mock()
-    # test run method when nothing to poll
-    interface.run()
-    assert logger.info.call_args_list == [call('SupvisorsEventInterface.run: entering main loop')]
-    assert not logger.debug.called
-    assert not logger.error.called
-    assert logger.warn.call_args_list == [call('SupvisorsEventInterface.run: exiting main loop')]
-    assert interface.subscriber.socket.closed
-
 
-def test_run_receive_error(mocker, context, logger, interface):
-    """ Test the behavior of the thread loop with exception on receive. """
-    # patch the context
-    mocker.patch.object(interface.stop_event, 'is_set', side_effect=[False, True])
-    mocker.patch('zmq.Poller.poll', return_value={interface.subscriber.socket: zmq.POLLIN})
-    mocker.patch.object(interface.subscriber, 'receive', side_effect=ZMQError(10, 'failed'))
-    logger.reset_mock()
-    # test run method with exception on receive
-    interface.run()
-    assert logger.info.call_args_list == [call('SupvisorsEventInterface.run: entering main loop')]
-    assert logger.debug.call_args_list == [call('SupvisorsEventInterface.run: got message on subscriber')]
-    assert logger.error.call_args_list == [call('SupvisorsEventInterface.run: failed to get data from subscriber:'
-                                                ' failed')]
-    assert logger.warn.call_args_list == [call('SupvisorsEventInterface.run: exiting main loop')]
-    assert interface.subscriber.socket.closed
 
-
-def test_run_receive_unexpected_event(mocker, context, logger, interface):
-    """ Test the behavior of the thread loop with Supvisors status message. """
-    # patch the context
-    mocker.patch.object(interface.stop_event, 'is_set', side_effect=[False, True])
-    mocker.patch('zmq.Poller.poll', return_value={interface.subscriber.socket: zmq.POLLIN})
-    mocker.patch.object(interface.subscriber, 'receive',
-                        return_value=('unexpected', 'supvisors payload'))
-    logger.reset_mock()
-    # test run method with Supvisors status message
-    interface.run()
-    assert logger.info.call_args_list == [call('SupvisorsEventInterface.run: entering main loop')]
-    assert logger.debug.call_args_list == [call('SupvisorsEventInterface.run: got message on subscriber')]
-    assert logger.error.call_args_list == [call('SupvisorsEventInterface.run: unexpected event type unexpected')]
-    assert logger.warn.call_args_list == [call('SupvisorsEventInterface.run: exiting main loop')]
-    assert interface.subscriber.socket.closed
-
-
-def test_run_receive_supvisors(mocker, context, logger, interface):
-    """ Test the behavior of the thread loop with Supvisors status message. """
-    # patch the context
-    mocker.patch.object(interface.stop_event, 'is_set', side_effect=[False, True])
-    mocker.patch('zmq.Poller.poll', return_value={interface.subscriber.socket: zmq.POLLIN})
-    mocker.patch.object(interface.subscriber, 'receive',
-                        return_value=(EventHeaders.SUPVISORS.value, 'supvisors payload'))
-    logger.reset_mock()
-    # test run method with Supvisors status message
-    interface.run()
-    assert logger.info.call_args_list == [call('SupvisorsEventInterface.run: entering main loop'),
-                                          call('SupvisorsEventInterface.on_supvisors_status:'
+def test_on_receive(interface, logger):
+    """ Dummy tests about logging when receiving messages from Supvisors. """
+    # test reception of Supvisors status message
+    interface.on_supvisors_status('supvisors payload')
+    assert logger.info.call_args_list == [call('SupvisorsZmqEventInterface.on_supvisors_status:'
                                                ' got Supvisors Status message: supvisors payload')]
-    assert logger.debug.call_args_list == [call('SupvisorsEventInterface.run: got message on subscriber')]
-    assert not logger.error.called
-    assert logger.warn.call_args_list == [call('SupvisorsEventInterface.run: exiting main loop')]
-    assert interface.subscriber.socket.closed
-
-
-def test_run_receive_instance(mocker, context, logger, interface):
-    """ Test the behavior of the thread loop with Instance status message. """
-    # patch the context
-    mocker.patch.object(interface.stop_event, 'is_set', side_effect=[False, True])
-    mocker.patch('zmq.Poller.poll', return_value={interface.subscriber.socket: zmq.POLLIN})
-    mocker.patch.object(interface.subscriber, 'receive',
-                        return_value=(EventHeaders.INSTANCE.value, 'instance payload'))
-    logger.reset_mock()
-    # test run method with Instance status message
-    interface.run()
-    assert logger.info.call_args_list == [call('SupvisorsEventInterface.run: entering main loop'),
-                                          call('SupvisorsEventInterface.on_instance_status:'
+    logger.info.reset_mock()
+    # test reception of Supvisors Instance status message
+    interface.on_instance_status('instance payload')
+    assert logger.info.call_args_list == [call('SupvisorsZmqEventInterface.on_instance_status:'
                                                ' got Instance Status message: instance payload')]
-    assert logger.debug.call_args_list == [call('SupvisorsEventInterface.run: got message on subscriber')]
-    assert not logger.error.called
-    assert logger.warn.call_args_list == [call('SupvisorsEventInterface.run: exiting main loop')]
-    assert interface.subscriber.socket.closed
-
-
-def test_run_receive_application(mocker, context, logger, interface):
-    """ Test the behavior of the thread loop with Application status message. """
-    # patch the context
-    mocker.patch.object(interface.stop_event, 'is_set', side_effect=[False, True])
-    mocker.patch('zmq.Poller.poll', return_value={interface.subscriber.socket: zmq.POLLIN})
-    mocker.patch.object(interface.subscriber, 'receive',
-                        return_value=(EventHeaders.APPLICATION.value, 'application payload'))
-    logger.reset_mock()
-    # test run method with Application status message
-    interface.run()
-    assert logger.info.call_args_list == [call('SupvisorsEventInterface.run: entering main loop'),
-                                          call('SupvisorsEventInterface.on_application_status:'
+    logger.info.reset_mock()
+    # test reception of Supvisors Application status message
+    interface.on_application_status('application payload')
+    assert logger.info.call_args_list == [call('SupvisorsZmqEventInterface.on_application_status:'
                                                ' got Application Status message: application payload')]
-    assert logger.debug.call_args_list == [call('SupvisorsEventInterface.run: got message on subscriber')]
-    assert not logger.error.called
-    assert logger.warn.call_args_list == [call('SupvisorsEventInterface.run: exiting main loop')]
-    assert interface.subscriber.socket.closed
-
-
-def test_run_receive_process(mocker, context, logger, interface):
-    """ Test the behavior of the thread loop with Process status message. """
-    # patch the context
-    mocker.patch.object(interface.stop_event, 'is_set', side_effect=[False, True])
-    mocker.patch('zmq.Poller.poll', return_value={interface.subscriber.socket: zmq.POLLIN})
-    mocker.patch.object(interface.subscriber, 'receive',
-                        return_value=(EventHeaders.PROCESS_STATUS.value, 'process payload'))
-    logger.reset_mock()
-    # test run method with Process status message
-    interface.run()
-    assert logger.info.call_args_list == [call('SupvisorsEventInterface.run: entering main loop'),
-                                          call('SupvisorsEventInterface.on_process_status:'
+    logger.info.reset_mock()
+    # test reception of Supvisors Process event message
+    interface.on_process_event('process event payload')
+    assert logger.info.call_args_list == [call('SupvisorsZmqEventInterface.on_process_event:'
+                                               ' got Process Event message: process event payload')]
+    logger.info.reset_mock()
+    # test reception of Supvisors Process status message
+    interface.on_process_status('process payload')
+    assert logger.info.call_args_list == [call('SupvisorsZmqEventInterface.on_process_status:'
                                                ' got Process Status message: process payload')]
-    assert logger.debug.call_args_list == [call('SupvisorsEventInterface.run: got message on subscriber')]
-    assert not logger.error.called
-    assert logger.warn.call_args_list == [call('SupvisorsEventInterface.run: exiting main loop')]
-    assert interface.subscriber.socket.closed
-
+    logger.info.reset_mock()
+    # test reception of Supvisors Application status message
+    interface.on_host_statistics('host statistics payload')
+    assert logger.info.call_args_list == [call('SupvisorsZmqEventInterface.on_host_statistics:'
+                                               ' got Host Statistics message: host statistics payload')]
+    logger.info.reset_mock()
+    # test reception of Supvisors Application status message
+    interface.on_process_statistics('process statistics payload')
+    assert logger.info.call_args_list == [call('SupvisorsZmqEventInterface.on_process_statistics:'
+                                               ' got Process Statistics message: process statistics payload')]
+    logger.info.reset_mock()
 
-def test_run_receive_event(mocker, context, logger, interface):
-    """ Test the behavior of the thread loop with Process event message. """
-    # patch the context
-    mocker.patch.object(interface.stop_event, 'is_set', side_effect=[False, True])
-    mocker.patch('zmq.Poller.poll', return_value={interface.subscriber.socket: zmq.POLLIN})
-    mocker.patch.object(interface.subscriber, 'receive',
-                        return_value=(EventHeaders.PROCESS_EVENT.value, 'event payload'))
-    logger.reset_mock()
-    # test run method with Process event message
-    interface.run()
-    assert logger.info.call_args_list == [call('SupvisorsEventInterface.run: entering main loop'),
-                                          call('SupvisorsEventInterface.on_process_event:'
-                                               ' got Process Event message: event payload')]
-    assert logger.debug.call_args_list == [call('SupvisorsEventInterface.run: got message on subscriber')]
-    assert not logger.error.called
-    assert logger.warn.call_args_list == [call('SupvisorsEventInterface.run: exiting main loop')]
-    assert interface.subscriber.socket.closed