Repository: ifm/nexxT (language: python)

Issue:
Issue33
This PR reverts the changes in https://github.com/ifm/nexxT/pull/30, which was having undesired side effects. Instead of fixing the deadlocking use case (which turned out to be pretty difficult) nexxT now detects cycles in the thread graph and refuses to start these configurations. 

Therefore, configurations working with nexxT <= 0.6.0 might not work anymore for higher versions. If such a situation occurs, it might be enough to change the filter's thread assignments. If this is not possible (because GUI threads might be involved), a redesign of some specific filters might be necessary.

Moreover, the change also reduces the latency introduced by inter-thread connections by reducing the number of samples allowed to be buffered in the QT event loop, such that the test case introduced for issue #33 passes.
Issue 25 deadlock
This PR addresses the deadlock issue described here https://github.com/ifm/nexxT/issues/25. Note the following side effects of this fix:

- The computational graph processing order is changed from depth first search to breadth first search
- Connections between filters running in the same thread are not processed immediately during the outputPort.transmit(...) function anymore. Instead, the transmitted samples are enqueued and processed later on.
- The InterThreadConnection class has been replaced by a PortToPortConnection class. This class handles both, intra- and inter-thread connections. The PortToPortConnection instances live in the threads of the output ports.
- A new class Executor is introduced, each thread has exactly one instance of this class. This class dispatches the enqueued transmit calls
   - PortToPortConnections pass onPortDataChanged events directly to the executor's queue, if the connection is intra-thread
   - In case of inter-thread connections, the connection's semaphore is tried to be acquired. If this succeeds, the onPortDataChanged event is enqueued in the input port's executor instance. Otherwise - to prevent the deadlocks - the output port's executor is allowed to process events (but the QT event loop will not be processed). 



Discussion / hints:

Fix patch:
diff --git a/nexxT/core/ActiveApplication.py b/nexxT/core/ActiveApplication.py
index 2b9435a..2fb03ad 100644
--- a/nexxT/core/ActiveApplication.py
+++ b/nexxT/core/ActiveApplication.py
@@ -11,9 +11,9 @@
 import logging
 from PySide2.QtCore import QObject, Slot, Signal, Qt, QCoreApplication
 from nexxT.interface import FilterState, OutputPortInterface, InputPortInterface
-from nexxT.core.Exceptions import FilterStateMachineError, NexTInternalError
+from nexxT.core.Exceptions import FilterStateMachineError, NexTInternalError, PossibleDeadlock
 from nexxT.core.CompositeFilter import CompositeFilter
-from nexxT.core.Utils import Barrier, assertMainThread
+from nexxT.core.Utils import Barrier, assertMainThread, MethodInvoker
 from nexxT.core.Thread import NexTThread
 
 logger = logging.getLogger(__name__) # pylint: disable=invalid-name
@@ -40,7 +40,7 @@ def __init__(self, graph):
         self._numThreadsSynced = 0
         self._state = FilterState.CONSTRUCTING
         self._graphConnected = False
-        self._portToPortConns = []
+        self._interThreadConns = []
         self._operationInProgress = False
         # connect signals and slots
         for tname in self._threads:
@@ -103,7 +103,7 @@ def cleanup(self):
         self._composite2graphs = {}
         # initialize private variables
         self._numThreadsSynced = 0
-        self._portToPortConns = []
+        self._interThreadConns = []
 
     def getState(self):
         """
@@ -232,6 +232,7 @@ def _setupConnections(self):
         :return: None
         """
         assertMainThread()
+        graph = {}
         if self._graphConnected:
             return
         for fromNode, fromPort, toNode, toPort in self._allConnections():
@@ -241,9 +242,28 @@ def _setupConnections(self):
             p0 = t0.getFilter(fromNode).getPort(fromPort, OutputPortInterface)
             t1 = self._threads[toThread]
             p1 = t1.getFilter(toNode).getPort(toPort, InputPortInterface)
-            p2pc = OutputPortInterface.setupPortToPortConnection(t0.getExecutor(), t1.getExecutor(), p0, p1)
-            p2pc.moveToThread(t0.qthread())
-            self._portToPortConns.append(p2pc)
+            if toThread == fromThread:
+                OutputPortInterface.setupDirectConnection(p0, p1)
+            else:
+                itc = OutputPortInterface.setupInterThreadConnection(p0, p1, self._threads[fromThread].qthread())
+                self._interThreadConns.append(itc)
+                if not fromThread in graph:
+                    graph[fromThread] = set()
+                if not toThread in graph:
+                    graph[toThread] = set()
+                graph[fromThread].add(toThread)
+
+        def _checkCycle(thread, cycleInfo):
+            if thread in cycleInfo:
+                cycle = "->".join(cycleInfo[cycleInfo.index(thread):] + [thread])
+                raise PossibleDeadlock(cycle)
+            cycle_info = cycleInfo + [thread]
+            for nt in graph[thread]:
+                _checkCycle(nt, cycle_info)
+
+        for thread in graph:
+            _checkCycle(thread, [])
+
         self._graphConnected = True
 
     @Slot()
@@ -348,8 +368,15 @@ def start(self):
             raise FilterStateMachineError(self._state, FilterState.STARTING)
         self._operationInProgress = True
         self._state = FilterState.STARTING
-        self._setupConnections()
-        for itc in self._portToPortConns:
+        try:
+            self._setupConnections()
+        except PossibleDeadlock as e:
+            self._state = FilterState.OPENED
+            MethodInvoker(self.close, Qt.QueuedConnection)
+            MethodInvoker(self.deinit, Qt.QueuedConnection)
+            logger.error(str(e))
+            return
+        for itc in self._interThreadConns:
             # set connections in active mode.
             itc.setStopped(False)
         self.performOperation.emit("start", Barrier(len(self._threads)))
@@ -373,7 +400,7 @@ def stop(self):
             raise FilterStateMachineError(self._state, FilterState.STOPPING)
         self._operationInProgress = True
         self._state = FilterState.STOPPING
-        for itc in self._portToPortConns:
+        for itc in self._interThreadConns:
             # set connections in active mode.
             itc.setStopped(True)
         self.performOperation.emit("stop", Barrier(len(self._threads)))
diff --git a/nexxT/core/Exceptions.py b/nexxT/core/Exceptions.py
index 1793354..c49d231 100644
--- a/nexxT/core/Exceptions.py
+++ b/nexxT/core/Exceptions.py
@@ -165,3 +165,10 @@ class CompositeRecursion(NexTRuntimeError):
     """
     def __init__(self, name):
         super().__init__("Composite filter '%s' depends on itself." % name)
+
+class PossibleDeadlock(NexTRuntimeError):
+    """
+    raised during application activation when a possible deadlock is detected (a cycle was found in the thread graph)
+    """
+    def __init__(self, cycle):
+        super().__init__("This graph is not deadlock-safe. A cycle has been found in the thread graph: %s" % cycle)
diff --git a/nexxT/core/Executor.py b/nexxT/core/Executor.py
deleted file mode 100644
index 26414fd..0000000
--- a/nexxT/core/Executor.py
+++ /dev/null
@@ -1,170 +0,0 @@
-# SPDX-License-Identifier: Apache-2.0
-# Copyright (C) 2020 ifm electronic gmbh
-#
-# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
-#
-
-"""
-This module defines the class Executor
-"""
-
-import logging
-from PySide2.QtCore import QObject, Signal, QThread, Qt, QMutex, QTimer
-import nexxT
-from nexxT.core.Utils import handleException
-
-logger = logging.getLogger(__name__)
-
-if nexxT.useCImpl:
-
-    import cnexxT
-
-    def Executor(*args): # pylint: disable=invalid-name
-        """
-        This is a factory function for the C version of the Executor class
-        """
-        res = cnexxT.nexxT.Executor.make_shared(cnexxT.nexxT.Executor(*args))
-        return res
-
-else:
-
-    class Executor(QObject):
-        """
-        Each nexxT thread has an executor for executing tasks (i.e. notifying filters about changes in the ports).
-        """
-
-        notify = Signal()
-        MAX_LOOPS_FINALIZE = 5
-
-        def __init__(self, qthread):
-            """
-            Construtor
-
-            :param qthread: the QThread instance this object shall be moved to
-            """
-            super().__init__()
-            self._pendingReceivesMutex = QMutex()
-            self._pendingReceives = []
-            self._blockedFilters = set()
-            self._stopped = False
-            self.moveToThread(qthread)
-            self.notify.connect(self.notifyInThread, Qt.QueuedConnection)
-
-        def registerPendingRcvSync(self, inputPort, dataSample):
-            """
-            Register a pending synchronous (i.e. originated from the same thread) receive event.
-
-            :param inputPort: The InputPort instance which shall be notified
-            :param dataSample: The DataSample instance to be delivered
-            """
-            self._registerPendingRcvSync(inputPort, dataSample)
-
-        @handleException
-        def _registerPendingRcvSync(self, inputPort, dataSample):
-            if not self._stopped:
-                self._pendingReceivesMutex.lock()
-                self._pendingReceives.append((inputPort, dataSample, None))
-                self._pendingReceivesMutex.unlock()
-                QTimer.singleShot(0, self.step)
-
-        def registerPendingRcvAsync(self, inputPort, dataSample, semaphore):
-            """
-            Register a pending asynchronous (i.e. originated from another thread through a PortToPortConnection
-            instacne) receive event.
-
-            :param inputPort: The InputPort instance which shall be notified
-            :param dataSample: The DataSample instance to be delivered
-            :param semaphore: The QSemaphore instance of the corresponding InterThreadConnection instance
-            """
-            self._registerPendingRcvAsync(inputPort, dataSample, semaphore)
-
-        @handleException
-        def _registerPendingRcvAsync(self, inputPort, dataSample, semaphore):
-            assert inputPort.thread() == self.thread()
-            if not self._stopped:
-                self._pendingReceivesMutex.lock()
-                self._pendingReceives.append((inputPort, dataSample, semaphore))
-                self._pendingReceivesMutex.unlock()
-                self.notify.emit()
-
-        def notifyInThread(self):
-            """
-            Slot called during _registerPendingRcvAsync, starts a single shot timer for the step function.
-            """
-            self._notifyInThread()
-
-        @handleException
-        def _notifyInThread(self):
-            QTimer.singleShot(0, self.step)
-
-        def step(self, fromFilter=None):
-            """
-            This function process one of the pending events.
-
-            :param fromFilter: An optional filter instance which will be blocked for further processing until the
-                function returns
-            :return: True if an event was processed, False otherwise.
-            """
-            return self._step(fromFilter)
-
-        @handleException
-        def _step(self, fromFilter):
-            assert QThread.currentThread() == self.thread()
-            res = False
-            if fromFilter is not None:
-                self._blockedFilters.add(fromFilter)
-            try:
-                if not self._stopped:
-                    self._pendingReceivesMutex.lock()
-                    for idx, (inputPort, dataSample, semaphore) in enumerate(self._pendingReceives):
-                        if inputPort.environment().getPlugin() not in self._blockedFilters:
-                            self._pendingReceives.pop(idx)
-                            self._pendingReceivesMutex.unlock()
-                            res = True
-                            if semaphore is None:
-                                inputPort.receiveSync(dataSample)
-                            else:
-                                inputPort.receiveAsync(dataSample, semaphore)
-                            # only process one sample
-                            break
-            finally:
-                if not res:
-                    self._pendingReceivesMutex.unlock()
-                if fromFilter is not None:
-                    self._blockedFilters.remove(fromFilter)
-            return res
-
-        def finalize(self):
-            """
-            This function processes the queue before the thread is stopped. In case of infinite recursions, an early
-            stop criterion is applied.
-            """
-            logger.internal("starting finalize (%s)", self.thread())
-            numCalled = {}
-            changed = True
-            while changed:
-                changed = False
-                self._pendingReceivesMutex.lock()
-                for idx, (inputPort, dataSample, semaphore) in enumerate(self._pendingReceives):
-                    if (inputPort.environment().getPlugin() not in self._blockedFilters and
-                            numCalled.get(inputPort, 0) < self.MAX_LOOPS_FINALIZE):
-                        self._pendingReceives.pop(idx)
-                        self._pendingReceivesMutex.unlock()
-                        numCalled[inputPort] = numCalled.get(inputPort, 0) + 1
-                        changed = True
-                        if semaphore is None:
-                            inputPort.receiveSync(dataSample)
-                        else:
-                            inputPort.receiveAsync(dataSample, semaphore)
-                        self._pendingReceivesMutex.lock()
-                        # only one sample, note that _pendingReceives is changed inside the loop!
-                        break
-                self._pendingReceivesMutex.unlock()
-
-        def clear(self):
-            """
-            Called after processing is stopped.
-            """
-            self._stopped = True
-            self._pendingReceives = []
-            self._blockedFilters = []
diff --git a/nexxT/core/PortImpl.py b/nexxT/core/PortImpl.py
index 91c06a5..8624c90 100644
--- a/nexxT/core/PortImpl.py
+++ b/nexxT/core/PortImpl.py
@@ -9,7 +9,7 @@
 """
 
 import logging
-from PySide2.QtCore import QThread, QSemaphore, QObject, Qt
+from PySide2.QtCore import QThread, QSemaphore, Signal, QObject, Qt
 from nexxT.interface.Ports import InputPortInterface, OutputPortInterface
 from nexxT.interface.DataSamples import DataSample
 from nexxT.interface.Services import Services
@@ -18,55 +18,35 @@
 
 logger = logging.getLogger(__name__)
 
-class PortToPortConnection(QObject):
+class InterThreadConnection(QObject):
     """
-    Helper class for transmitting data samples between output and input ports
+    Helper class for transmitting data samples between threads
     """
+    transmitInterThread = Signal(object, QSemaphore)
 
-    def __init__(self, executorFrom, executorTo, portFrom, portTo):
-        """
-        Constructor.
-
-        :param executorFrom: executor instance for portFrom
-        :param executorTo: executorInstance for portTo
-        :param portFrom: the outut port which transmits the data
-        :param portTo: the input port which receives the data
-        """
+    def __init__(self, qthread_from):
         super().__init__()
-        self._semaphore = QSemaphore(1)
+        self.moveToThread(qthread_from)
+        self.semaphore = QSemaphore(1)
         self._stopped = True
-        self._executorFrom = executorFrom
-        self._executorTo = executorTo
-        self._portFrom = portFrom
-        self._portTo = portTo
 
     def receiveSample(self, dataSample):
         """
         Receive a sample, called in the source's thread. Uses a semaphore to avoid buffering infinitely.
-
         :param dataSample: the sample to be received
         :return: None
         """
-        if self._executorFrom is self._executorTo:
-            self._executorTo.registerPendingRcvSync(self._portTo, dataSample)
-        else:
-            self._receiveSample(dataSample)
+        self._receiveSample(dataSample)
 
     @handleException
     def _receiveSample(self, dataSample):
         assert QThread.currentThread() is self.thread()
-        timeout = 0
         while True:
             if self._stopped:
-                logger.warning("The inter-thread connection is set to stopped mode; data sample discarded.")
+                logger.info("The inter-thread connection is set to stopped mode; data sample discarded.")
                 break
-            if not self._semaphore.tryAcquire(1, timeout):
-                if self._executorFrom.step(self._portFrom.environment().getPlugin()):
-                    timeout = 0
-                else:
-                    timeout = 10 #ms
-            else:
-                self._executorTo.registerPendingRcvAsync(self._portTo, dataSample, self._semaphore)
+            if self.semaphore.tryAcquire(1, 500):
+                self.transmitInterThread.emit(dataSample, self.semaphore)
                 break
 
     def setStopped(self, stopped):
@@ -108,22 +88,34 @@ def clone(self, newEnvironment):
         return OutputPortImpl(self.dynamic(), self.name(), newEnvironment)
 
     @staticmethod
-    def setupPortToPortConnection(executorFrom, executorTo, outputPort, inputPort):
+    def setupDirectConnection(outputPort, inputPort):
         """
-        Setup a port to port connection between outputPort and inputPort
+        Setup a direct (intra-thread) connection between outputPort and inputPort
+        Note: both instances must live in same thread!
 
-        :param executorFrom: the executor instance of the outputPort's thread
-        :param executorTo: the executor instacne of the inputPort's thread
         :param outputPort: the output port instance to be connected
         :param inputPort: the input port instance to be connected
-        :return: an PortToPortConncetion instance which manages the connection (has
+        :return:None
+        """
+        logger.info("setup direct connection between %s -> %s", outputPort.name(), inputPort.name())
+        outputPort.transmitSample.connect(inputPort.receiveSync, Qt.DirectConnection)
+
+    @staticmethod
+    def setupInterThreadConnection(outputPort, inputPort, outputPortThread):
+        """
+        Setup an inter thread connection between outputPort and inputPort
+
+        :param outputPort: the output port instance to be connected
+        :param inputPort: the input port instance to be connected
+        :param outputPortThread: the QThread instance of the outputPort instance
+        :return: an InterThreadConnection instance which manages the connection (has
                  to survive until connections is deleted)
         """
-        p2pc = PortToPortConnection(executorFrom, executorTo, outputPort, inputPort)
-        assert inputPort.thread() == executorTo.thread()
-        assert outputPort.thread() == executorFrom.thread()
-        outputPort.transmitSample.connect(p2pc.receiveSample, Qt.DirectConnection)
-        return p2pc
+        logger.info("setup inter thread connection between %s -> %s", outputPort.name(), inputPort.name())
+        itc = InterThreadConnection(outputPortThread)
+        outputPort.transmitSample.connect(itc.receiveSample, Qt.DirectConnection)
+        itc.transmitInterThread.connect(inputPort.receiveAsync, Qt.QueuedConnection)
+        return itc
 
 class InputPortImpl(InputPortInterface):
     """
@@ -216,8 +208,8 @@ def _receiveAsync(self, dataSample, semaphore):
         self._addToQueue(dataSample)
         if not self._interthreadDynamicQueue:
             # usual behaviour
-            semaphore.release(1)
             self._transmit()
+            semaphore.release(1)
         else:
             if semaphore not in self._semaphoreN:
                 self._semaphoreN[semaphore] = 1
diff --git a/nexxT/core/Thread.py b/nexxT/core/Thread.py
index e544146..6877cbe 100644
--- a/nexxT/core/Thread.py
+++ b/nexxT/core/Thread.py
@@ -14,7 +14,6 @@
 from PySide2.QtCore import QObject, Signal, Slot, QCoreApplication, QThread
 from nexxT.interface import FilterState, Services
 from nexxT.core.Exceptions import NodeExistsError, NexTInternalError, NodeNotFoundError, NexTRuntimeError
-from nexxT.core.Executor import Executor
 from nexxT.core.Utils import handleException
 
 logger = logging.getLogger(__name__)
@@ -91,7 +90,6 @@ def __init__(self, name):
             self._qthread.start()
         self.moveToThread(self._qthread)
         self.cleanUpCalled = False
-        self._executor = Executor(self._qthread)
 
     def __del__(self):
         logger.debug("destructor of Thread")
@@ -111,8 +109,6 @@ def cleanup(self):
             self._qthread.quit()
             self._qthread.wait()
             self._qthread = None
-        logger.internal("cleanup executor")
-        self._executor.clear()
         logger.internal("cleanup filters")
         for name in self._filters:
             self._filters[name].destroy()
@@ -154,14 +150,6 @@ def getName(self, filterEnvironment):
             raise NexTRuntimeError("Filterenvironment not found. Not active?")
         return self._filter2name[filterEnvironment]
 
-    def getExecutor(self):
-        """
-        Returns the executor instance of this thread.
-
-        :return: An Executor instance
-        """
-        return self._executor
-
     def qthread(self):
         """
         Return the corresponding qthread.
@@ -179,8 +167,6 @@ def performOperation(self, operation, barrier):
         """
         # wait that all threads are in their event loop.
         barrier.wait()
-        if operation == "stop":
-            self._executor.finalize()
         if operation in self._operations:
             # pre-adaptation of states (e.g. from CONSTRUCTED to INITIALIZING)
             # before one of the actual operations is called, all filters are in the adapted state
diff --git a/nexxT/core/qrc_resources.py b/nexxT/core/qrc_resources.py
index c9abf80..683caf5 100644
--- a/nexxT/core/qrc_resources.py
+++ b/nexxT/core/qrc_resources.py
@@ -1,278 +1,278 @@
-# Resource object code (Python 3)
-# Created by: object code
-# Created by: The Resource Compiler for Qt version 5.15.1
-# WARNING! All changes made in this file will be lost!
-
-from PySide2 import QtCore
-
-qt_resource_data = b"\
-\x00\x00\x08d\
-<\
-?xml version=\x221.\
-0\x22 encoding=\x22UTF\
--8\x22 standalone=\x22\
-no\x22?>\x0a<!-- Creat\
-ed with Inkscape\
- (http://www.ink\
-scape.org/) -->\x0a\
-\x0a<svg\x0a   xmlns:d\
-c=\x22http://purl.o\
-rg/dc/elements/1\
-.1/\x22\x0a   xmlns:cc\
-=\x22http://creativ\
-ecommons.org/ns#\
-\x22\x0a   xmlns:rdf=\x22\
-http://www.w3.or\
-g/1999/02/22-rdf\
--syntax-ns#\x22\x0a   \
-xmlns:svg=\x22http:\
-//www.w3.org/200\
-0/svg\x22\x0a   xmlns=\
-\x22http://www.w3.o\
-rg/2000/svg\x22\x0a   \
-xmlns:sodipodi=\x22\
-http://sodipodi.\
-sourceforge.net/\
-DTD/sodipodi-0.d\
-td\x22\x0a   xmlns:ink\
-scape=\x22http://ww\
-w.inkscape.org/n\
-amespaces/inksca\
-pe\x22\x0a   width=\x2223\
-.818361mm\x22\x0a   he\
-ight=\x2223.016552m\
-m\x22\x0a   viewBox=\x220\
- 0 23.818361 23.\
-016552\x22\x0a   versi\
-on=\x221.1\x22\x0a   id=\x22\
-svg8\x22\x0a   inkscap\
-e:version=\x220.92.\
-4 (5da689c313, 2\
-019-01-14)\x22\x0a   s\
-odipodi:docname=\
-\x22media-record.sv\
-g\x22>\x0a  <defs\x0a    \
- id=\x22defs2\x22 />\x0a \
- <sodipodi:named\
-view\x0a     id=\x22ba\
-se\x22\x0a     pagecol\
-or=\x22#ffffff\x22\x0a   \
-  bordercolor=\x22#\
-666666\x22\x0a     bor\
-deropacity=\x221.0\x22\
-\x0a     inkscape:p\
-ageopacity=\x220.0\x22\
-\x0a     inkscape:p\
-ageshadow=\x222\x22\x0a  \
-   inkscape:zoom\
-=\x221.979899\x22\x0a    \
- inkscape:cx=\x2286\
-.156501\x22\x0a     in\
-kscape:cy=\x2240.84\
-2299\x22\x0a     inksc\
-ape:document-uni\
-ts=\x22mm\x22\x0a     ink\
-scape:current-la\
-yer=\x22layer1\x22\x0a   \
-  showgrid=\x22fals\
-e\x22\x0a     inkscape\
-:window-width=\x221\
-920\x22\x0a     inksca\
-pe:window-height\
-=\x22995\x22\x0a     inks\
-cape:window-x=\x220\
-\x22\x0a     inkscape:\
-window-y=\x220\x22\x0a   \
-  inkscape:windo\
-w-maximized=\x221\x22 \
-/>\x0a  <metadata\x0a \
-    id=\x22metadata\
-5\x22>\x0a    <rdf:RDF\
->\x0a      <cc:Work\
-\x0a         rdf:ab\
-out=\x22\x22>\x0a        \
-<dc:format>image\
-/svg+xml</dc:for\
-mat>\x0a        <dc\
-:type\x0a          \
- rdf:resource=\x22h\
-ttp://purl.org/d\
-c/dcmitype/Still\
-Image\x22 />\x0a      \
-  <dc:title></dc\
-:title>\x0a      </\
-cc:Work>\x0a    </r\
-df:RDF>\x0a  </meta\
-data>\x0a  <g\x0a     \
-inkscape:label=\x22\
-Layer 1\x22\x0a     in\
-kscape:groupmode\
-=\x22layer\x22\x0a     id\
-=\x22layer1\x22\x0a     t\
-ransform=\x22transl\
-ate(-20.430431,-\
-27.309361)\x22>\x0a   \
- <circle\x0a       \
-style=\x22fill:#ff0\
-000;stroke:#0000\
-00;stroke-width:\
-0.82299995;strok\
-e-miterlimit:4;s\
-troke-dasharray:\
-none;stroke-opac\
-ity:1\x22\x0a       id\
-=\x22path817\x22\x0a     \
-  cx=\x2232.339611\x22\
-\x0a       cy=\x2238.5\
-50369\x22\x0a       r=\
-\x227.2162771\x22 />\x0a \
-   <rect\x0a       \
-style=\x22fill:none\
-;stroke:none;str\
-oke-width:0.5243\
-5654;stroke-mite\
-rlimit:4;stroke-\
-dasharray:none;s\
-troke-opacity:1\x22\
-\x0a       id=\x22rect\
-819\x22\x0a       widt\
-h=\x2223.818361\x22\x0a  \
-     height=\x2223.\
-016552\x22\x0a       x\
-=\x2220.430431\x22\x0a   \
-    y=\x2227.309361\
-\x22 />\x0a  </g>\x0a</sv\
-g>\x0a\
-\x00\x00\x05\xf9\
-\x00\
-\x00\x1f#x\x9c\xedY[o\xdb6\x14~\xcf\xaf\xd0\
-\x94\x87\xb6X)\x91\xbaK\xb1]l+\x0a\x14\x18\xf6\
-\xb0e\xd83-\xd16\x11I4(:\xb6\xfb\xebw\
-H\xebb\xf92d\x08:\x04\x83\x0d\x04\xb1\xce\x95\xe7\
-\x9c\x8f\x9f\xc8d\xf2iW\x95\xd63\x93\x0d\x17\xf5\xd4\
-&\x0e\xb6-V\xe7\xa2\xe0\xf5rj\xff\xf9\xf8\x05%\
-\xb6\xd5(Z\x17\xb4\x145\x9b\xda\xb5\xb0?\xcd\xee&\
-? d\xfd\x22\x19U\xac\xb0\xb6\x5c\xad\xac\xaf\xf5S\
-\x93\xd35\xb3\xde\xaf\x94Zg\xae\xbb\xddn\x1d\xde\x0a\
-\x1d!\x97\xee\x07\x0b\xa1\xd9\xdd\xdd\xa4y^\xdeY\x96\
-\x05y\xeb&+\xf2\xa9\xdd:\xac7\xb24\x86E\xee\
-\xb2\x92U\xacV\x8dK\x1c\xe2\xda\x83y>\x98\xe7:\
-;\x7ff\xb9\xa8*Q7\xc6\xb3n\xee\x8f\x8ce\xb1\
-\xe8\xad\xf5j\xb6\xbe1\x22i\x9a\xba\xd8s=\x0f\x81\
-\x05j\xf6\xb5\xa2;4v\x855^r\xf50\xc6.\
-\xe8\x06\xcb\x97Ye\x0d4t\x0d?\xbdy'p\x1a\
-\xb1\x919[\x80\x1fsj\xa6\xdc\xcf\x8f\x9f{%\xc2\
-N\xa1\x8a\xa30]?GYGM\xaei\xc5\x9a5\
-\xcdY\xe3vr\xe3\xbf\xe5\x85ZMm/p|\xec\
-\x93\xd4\xab*#^1\xbe\x5c\xa9s\xf93g\xdb\x9f\
-\xc5njc\x0b[\xbdr\xf8v0\x1aPC\x8c\x80\
-\x17S\x1b\xeaN\x0e\x0fm\xfa\xac7\xc3N\xea9\x81\
-\xf5>,h\x94\xa4\xb9O\xfc\x8f\x96\x87I\x8a0A\
-$\xf8`\xbc\xba\xd2\xb3B\xe4\xba\x16\x00\x1c\xdb\xed\x1e\
-\x9d\xae\x9d}T\xb6[\x0b\xa9\xd0\x82\x97\xec`\xe7\xae\
-D\xc5\xdc-g\x05\xabh\xed\x16\xec\x99\x95b\xadQ\
-\xe4\x9a\x10\xeeV\xc8'\xd3\x9a\xc33\x8a\x02g]_\
-\x8e\xba+\xd60\xaa(r\x92\x04\xc0\x92^\xb4\xd9\x9f\
-\xd8\xcc\xc0hR\xb0E\xa3\x8d\x0f\xbd\xd0O\x9em\xb9\
-F\xd5W\xa6\x97[\xe8\x06\x0f\x86s\xda\x1c\xc6dY\
-k\xba\x04H\x97BN\xed\xfb\x85\xf9\xb4\x8a\xb9\x90\x05\
-\x93\x9d*2\x9f\x91J@m\x5c\xed\x0f\x9b\xb8\x8d\xdd\
-\xadYG\xed\xf5\xf8\xb2\xbeY\xd1Bl\x01\x0b\xa7\xca\
-oBT\xda+\x08\xbd0H\xfc\xe0T\x9f\x03N\x10\
-(\x1c\x92\xe0(>\xd3BF/\x0e\x9c\x14\xfbqx\
-\xaa\x84)o\xf4\x88\xd0\xa6\xe6\x0a6S\x0b\xbfc\xf7\
-\x8d\x94\xda\xa0\xa4{\x06\x85\x9b_\xa45jVb\xbb\
-\x94\xba\x81\x0bZ\xf6\x1d\xec]\xb7\xbc\x86\x82P\x0b}\
-@\xedY\xd9\xadE\xb7\x0b\xd2\xf4l\x81\xad\x85\xde\x09\
-WT\xfb\xeb\xaa\x8a\xeex\xc5\xbf1X 1\xf8\x00\
-\x18\xf46z\xe1\x077\xcbR{\xbd\xa7w{-\xb3\
-;\xa1\xaeK\x0b\x820\xf4z\xa1\x90|\xc9k\xd3p\
-\xcf\xc1\xb1\x87\x03r\xa2\x83\xf5 \x12\xc5\x8e\x1f\xf9Q\
-\xda\x81\xcf=G\x9f\x91WL\xd1\x82*:@\xb1\x93\
-\x84\xdd\x82\x81#\xb3\xdf?\x7f\x99\xb5Y&y\x9e\xfd\
-\x05;\xa9KjY\xda\x80\xce\xc5\x06\x1ah\xcfz\xf1\
-\xa4\xc83`\xb5\x8a\xaa\x19\xaf\x00]\x9a\x10\x7f\x04\x16\
-\x9b\xb8\x83bd\xac{0\x04=\x84\x95\xec@\x8f\x17\
-\xdf\x11E^q\xed\xe4\xfe\xa1xY~\xd5I\xdar\
-\x8f\x82rU\xb2\x99\xc9y\xf8\xdaU\xe1\xb6e\xb4E\
-\xbaGUN\xdc\xae\x07\xe6iy2\xdb\x92\xceY9\
-\xb5\x7f\xd5@\xb4\xc8\xe9\xe4\x97Rl\xd6\x95(X\x0b\
-U{\xe8\xec\x08\xbaJ\xd2\xba\xd1m\x98\xda\xe6k\x09\
-o\xd2\xf7\xc3L?\x22\x82C\xc7\xf7}\xe2}\xe8\x06\
-\x91s\x99\x97}\x8b\x1a\xb5/!\x09\xf0_\x99\xdd\xe7\
-\xc9\x9c\xcd\xe3\x87FI\xf1\xc4\xb2{l>\xed\xe3\x01\
-\xff\x19v\x12\xcf\xd7b\xd2\xc9\xa1{L\x96\x00P\x95\
-\x05\x9d\xac\xa0\xc0\x02R\xd2}V\xc3\xdb\xbe\x93\xb6\xcc\
-\x91\x91\x116\xd7T\xad\x02 \x84^\xa8y \x0c\x1c\
-\xcf\x03\xe8\xc5\x83T3\x12\x89\x9d \x09\xe2\x01\xc7R\
-\x0b\x9d8\xc08\x8d\xfa\xa9M\x14\xdb\xa9\xce\x02\xb0\x92\
-\x19\xba\x86L\x80\x03&\x9f\x99}Z\xbc\x00b0\xdf\
-a\xb9\x80\xa8\xf2\xc1H\x9e\xa9\xe4\xb4V#\xd9\xd6l\
-\xf1\x91\x08\x8ac*_\x8de\xb0[3\x82\x9d0\x81\
-\xde\xfb\xd8[\xef\x1eJ^\xb3\x96!2\xe2x\xe1\xc1\
-pA+^\xee\xb3w?A\xaa\xd2\xfa\xb9\xa4\xf9\xd3\
-\xbb\x07\xd4\xc1\x00\x1d\x82\xadY\xce\x17<\x87c\x8a\xa8\
-G\xa6\x1f\xad\xdfL\xd2w\xa3\x05\xa3\x92/\xa9\xda@\
-\xb1\x97\xcaA\x10\xf8\xb2\xa2\x06\x0a\x95<\x1f\xe9\x16\xcc\
-DB\x0dS\x0a\x8er\xbd\xa3\xee0\xa2\x90\xa8\xce\xe0\
-T'\xd5C\x09\x06L\x22\xddi\xb0\xcb0T\x0c/\
-\xcab,\x90\x5c\x07A\x1a\xd7Y)\x91\x9a\xb7q\xea\
-|%d\x1b\xe8\x80DJ\x03\x98\xa9y\x18`\xd3\x01\
-\xf3\x18S\x1d*\xbd(\xd0\xcd\x1e\x90\xa1\xb9\x8dxp\
-\xa4 q\x90\xf6R\x00Q\x88\x1d\x12c\x12\xc4#\x14\
-\xea\x85\xc0\x8b)\xe8\x85G;K\x0ae\xb6U\x8aa\
-\x13M\x14\x94T\x0f\x0c\xd33\xa2\x14\x1aKz\xcc\xf6\
-\xa05\xa1\xb5\x03\xc4\xf6\x8e\xe4\x97Wwe}\xff-\
-P\xff_\xb8\xfc\xf7\x98\xbb\x06\xacY=q\xcd(\xe1\
-=\xa0C\xdc\xb8\xe6\xc65\xc3n\x86]\x1b\xc5q\x1a\
-\xf9\xc7TC\x88\xe7\xa4Q\xe8\x91KT\x93\xbc\x86K\
-\xa21\x97\x9cg\xbf\x92\xffF%o\x82J\xd8\x8dJ\
-\xde\xc6L\xdf\x22\x95\x10\x12\xc0\xf1\xdd\x8f\xc2c*A\
-a\x02'b\x12\x90\xe8\x02\x97\x1c\xf9\xf77\x88\xfe\xfc\
-\x82r\xb8\xf7B\x8d\xe6FO\x12/\x89\xbd\x97\x98\xeb\
-\x13\x0a\x5c#\xd2 H\xc2\xe8\x1fNE\xaf;\x14\xf9\
-xLd\xe7\xb5_\xab\xfe\xc6do\x82\xc9v7&\
-{\x1b3}\x8bL\x86\xc2\x18\xce%$\x8c\xbc\x11\x95\
-\xe9\x8bOB\xfc$\xbcDe\xd1\x0b\xb9\x092\xfa$\
-\x0cB\x1c\xbe\x8c\xfb\x10\xcc\x1cG\x01N\xc9%2\x03\
-\xbf\x12nx\xe4U\x5c\x16\x9c\x5c\xf0\xce\xab\xbfV\xff\
-\x8d\xccndv#\xb3\xefFf\xed\x1f2_\x7f\xc3\
-\xf3\x03\xdfOF7<\xd8\xca\xbew\xfc\xb7\x9d\x81\xca\
-\x02\xfc\x1a2I\xceox\xe3\xecW\xf2\xdf\xa8\xe4;\
-S\xc9U\xf6x<g\x8f\x89\xbb\x9c\xddM\xf4\xff,\
-fw\x7f\x038y\x18\x8e\
-"
-
-qt_resource_name = b"\
-\x00\x05\
-\x00o\xa6S\
-\x00i\
-\x00c\x00o\x00n\x00s\
-\x00\x10\
-\x08\x12\xa3'\
-\x00m\
-\x00e\x00d\x00i\x00a\x00-\x00r\x00e\x00c\x00o\x00r\x00d\x00.\x00s\x00v\x00g\
-\x00\x09\
-\x0f\xd7\xb3G\
-\x00n\
-\x00e\x00x\x00x\x00T\x00.\x00s\x00v\x00g\
-"
-
-qt_resource_struct = b"\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
-\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
-\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x01v\xc7b\x82\xe4\
-\x00\x00\x006\x00\x01\x00\x00\x00\x01\x00\x00\x08h\
-\x00\x00\x01v\xc7b\x82\xe4\
-"
-
-def qInitResources():
-    QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
-
-def qCleanupResources():
-    QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
-
-qInitResources()
+# Resource object code (Python 3)
+# Created by: object code
+# Created by: The Resource Compiler for Qt version 5.15.1
+# WARNING! All changes made in this file will be lost!
+
+from PySide2 import QtCore
+
+qt_resource_data = b"\
+\x00\x00\x08d\
+<\
+?xml version=\x221.\
+0\x22 encoding=\x22UTF\
+-8\x22 standalone=\x22\
+no\x22?>\x0a<!-- Creat\
+ed with Inkscape\
+ (http://www.ink\
+scape.org/) -->\x0a\
+\x0a<svg\x0a   xmlns:d\
+c=\x22http://purl.o\
+rg/dc/elements/1\
+.1/\x22\x0a   xmlns:cc\
+=\x22http://creativ\
+ecommons.org/ns#\
+\x22\x0a   xmlns:rdf=\x22\
+http://www.w3.or\
+g/1999/02/22-rdf\
+-syntax-ns#\x22\x0a   \
+xmlns:svg=\x22http:\
+//www.w3.org/200\
+0/svg\x22\x0a   xmlns=\
+\x22http://www.w3.o\
+rg/2000/svg\x22\x0a   \
+xmlns:sodipodi=\x22\
+http://sodipodi.\
+sourceforge.net/\
+DTD/sodipodi-0.d\
+td\x22\x0a   xmlns:ink\
+scape=\x22http://ww\
+w.inkscape.org/n\
+amespaces/inksca\
+pe\x22\x0a   width=\x2223\
+.818361mm\x22\x0a   he\
+ight=\x2223.016552m\
+m\x22\x0a   viewBox=\x220\
+ 0 23.818361 23.\
+016552\x22\x0a   versi\
+on=\x221.1\x22\x0a   id=\x22\
+svg8\x22\x0a   inkscap\
+e:version=\x220.92.\
+4 (5da689c313, 2\
+019-01-14)\x22\x0a   s\
+odipodi:docname=\
+\x22media-record.sv\
+g\x22>\x0a  <defs\x0a    \
+ id=\x22defs2\x22 />\x0a \
+ <sodipodi:named\
+view\x0a     id=\x22ba\
+se\x22\x0a     pagecol\
+or=\x22#ffffff\x22\x0a   \
+  bordercolor=\x22#\
+666666\x22\x0a     bor\
+deropacity=\x221.0\x22\
+\x0a     inkscape:p\
+ageopacity=\x220.0\x22\
+\x0a     inkscape:p\
+ageshadow=\x222\x22\x0a  \
+   inkscape:zoom\
+=\x221.979899\x22\x0a    \
+ inkscape:cx=\x2286\
+.156501\x22\x0a     in\
+kscape:cy=\x2240.84\
+2299\x22\x0a     inksc\
+ape:document-uni\
+ts=\x22mm\x22\x0a     ink\
+scape:current-la\
+yer=\x22layer1\x22\x0a   \
+  showgrid=\x22fals\
+e\x22\x0a     inkscape\
+:window-width=\x221\
+920\x22\x0a     inksca\
+pe:window-height\
+=\x22995\x22\x0a     inks\
+cape:window-x=\x220\
+\x22\x0a     inkscape:\
+window-y=\x220\x22\x0a   \
+  inkscape:windo\
+w-maximized=\x221\x22 \
+/>\x0a  <metadata\x0a \
+    id=\x22metadata\
+5\x22>\x0a    <rdf:RDF\
+>\x0a      <cc:Work\
+\x0a         rdf:ab\
+out=\x22\x22>\x0a        \
+<dc:format>image\
+/svg+xml</dc:for\
+mat>\x0a        <dc\
+:type\x0a          \
+ rdf:resource=\x22h\
+ttp://purl.org/d\
+c/dcmitype/Still\
+Image\x22 />\x0a      \
+  <dc:title></dc\
+:title>\x0a      </\
+cc:Work>\x0a    </r\
+df:RDF>\x0a  </meta\
+data>\x0a  <g\x0a     \
+inkscape:label=\x22\
+Layer 1\x22\x0a     in\
+kscape:groupmode\
+=\x22layer\x22\x0a     id\
+=\x22layer1\x22\x0a     t\
+ransform=\x22transl\
+ate(-20.430431,-\
+27.309361)\x22>\x0a   \
+ <circle\x0a       \
+style=\x22fill:#ff0\
+000;stroke:#0000\
+00;stroke-width:\
+0.82299995;strok\
+e-miterlimit:4;s\
+troke-dasharray:\
+none;stroke-opac\
+ity:1\x22\x0a       id\
+=\x22path817\x22\x0a     \
+  cx=\x2232.339611\x22\
+\x0a       cy=\x2238.5\
+50369\x22\x0a       r=\
+\x227.2162771\x22 />\x0a \
+   <rect\x0a       \
+style=\x22fill:none\
+;stroke:none;str\
+oke-width:0.5243\
+5654;stroke-mite\
+rlimit:4;stroke-\
+dasharray:none;s\
+troke-opacity:1\x22\
+\x0a       id=\x22rect\
+819\x22\x0a       widt\
+h=\x2223.818361\x22\x0a  \
+     height=\x2223.\
+016552\x22\x0a       x\
+=\x2220.430431\x22\x0a   \
+    y=\x2227.309361\
+\x22 />\x0a  </g>\x0a</sv\
+g>\x0a\
+\x00\x00\x05\xf9\
+\x00\
+\x00\x1f#x\x9c\xedY[o\xdb6\x14~\xcf\xaf\xd0\
+\x94\x87\xb6X)\x91\xbaK\xb1]l+\x0a\x14\x18\xf6\
+\xb0e\xd83-\xd16\x11I4(:\xb6\xfb\xebw\
+H\xebb\xf92d\x08:\x04\x83\x0d\x04\xb1\xce\x95\xe7\
+\x9c\x8f\x9f\xc8d\xf2iW\x95\xd63\x93\x0d\x17\xf5\xd4\
+&\x0e\xb6-V\xe7\xa2\xe0\xf5rj\xff\xf9\xf8\x05%\
+\xb6\xd5(Z\x17\xb4\x145\x9b\xda\xb5\xb0?\xcd\xee&\
+? d\xfd\x22\x19U\xac\xb0\xb6\x5c\xad\xac\xaf\xf5S\
+\x93\xd35\xb3\xde\xaf\x94Zg\xae\xbb\xddn\x1d\xde\x0a\
+\x1d!\x97\xee\x07\x0b\xa1\xd9\xdd\xdd\xa4y^\xdeY\x96\
+\x05y\xeb&+\xf2\xa9\xdd:\xac7\xb24\x86E\xee\
+\xb2\x92U\xacV\x8dK\x1c\xe2\xda\x83y>\x98\xe7:\
+;\x7ff\xb9\xa8*Q7\xc6\xb3n\xee\x8f\x8ce\xb1\
+\xe8\xad\xf5j\xb6\xbe1\x22i\x9a\xba\xd8s=\x0f\x81\
+\x05j\xf6\xb5\xa2;4v\x855^r\xf50\xc6.\
+\xe8\x06\xcb\x97Ye\x0d4t\x0d?\xbdy'p\x1a\
+\xb1\x919[\x80\x1fsj\xa6\xdc\xcf\x8f\x9f{%\xc2\
+N\xa1\x8a\xa30]?GYGM\xaei\xc5\x9a5\
+\xcdY\xe3vr\xe3\xbf\xe5\x85ZMm/p|\xec\
+\x93\xd4\xab*#^1\xbe\x5c\xa9s\xf93g\xdb\x9f\
+\xc5njc\x0b[\xbdr\xf8v0\x1aPC\x8c\x80\
+\x17S\x1b\xeaN\x0e\x0fm\xfa\xac7\xc3N\xea9\x81\
+\xf5>,h\x94\xa4\xb9O\xfc\x8f\x96\x87I\x8a0A\
+$\xf8`\xbc\xba\xd2\xb3B\xe4\xba\x16\x00\x1c\xdb\xed\x1e\
+\x9d\xae\x9d}T\xb6[\x0b\xa9\xd0\x82\x97\xec`\xe7\xae\
+D\xc5\xdc-g\x05\xabh\xed\x16\xec\x99\x95b\xadQ\
+\xe4\x9a\x10\xeeV\xc8'\xd3\x9a\xc33\x8a\x02g]_\
+\x8e\xba+\xd60\xaa(r\x92\x04\xc0\x92^\xb4\xd9\x9f\
+\xd8\xcc\xc0hR\xb0E\xa3\x8d\x0f\xbd\xd0O\x9em\xb9\
+F\xd5W\xa6\x97[\xe8\x06\x0f\x86s\xda\x1c\xc6dY\
+k\xba\x04H\x97BN\xed\xfb\x85\xf9\xb4\x8a\xb9\x90\x05\
+\x93\x9d*2\x9f\x91J@m\x5c\xed\x0f\x9b\xb8\x8d\xdd\
+\xadYG\xed\xf5\xf8\xb2\xbeY\xd1Bl\x01\x0b\xa7\xca\
+oBT\xda+\x08\xbd0H\xfc\xe0T\x9f\x03N\x10\
+(\x1c\x92\xe0(>\xd3BF/\x0e\x9c\x14\xfbqx\
+\xaa\x84)o\xf4\x88\xd0\xa6\xe6\x0a6S\x0b\xbfc\xf7\
+\x8d\x94\xda\xa0\xa4{\x06\x85\x9b_\xa45jVb\xbb\
+\x94\xba\x81\x0bZ\xf6\x1d\xec]\xb7\xbc\x86\x82P\x0b}\
+@\xedY\xd9\xadE\xb7\x0b\xd2\xf4l\x81\xad\x85\xde\x09\
+WT\xfb\xeb\xaa\x8a\xeex\xc5\xbf1X 1\xf8\x00\
+\x18\xf46z\xe1\x077\xcbR{\xbd\xa7w{-\xb3\
+;\xa1\xaeK\x0b\x820\xf4z\xa1\x90|\xc9k\xd3p\
+\xcf\xc1\xb1\x87\x03r\xa2\x83\xf5 \x12\xc5\x8e\x1f\xf9Q\
+\xda\x81\xcf=G\x9f\x91WL\xd1\x82*:@\xb1\x93\
+\x84\xdd\x82\x81#\xb3\xdf?\x7f\x99\xb5Y&y\x9e\xfd\
+\x05;\xa9KjY\xda\x80\xce\xc5\x06\x1ah\xcfz\xf1\
+\xa4\xc83`\xb5\x8a\xaa\x19\xaf\x00]\x9a\x10\x7f\x04\x16\
+\x9b\xb8\x83bd\xac{0\x04=\x84\x95\xec@\x8f\x17\
+\xdf\x11E^q\xed\xe4\xfe\xa1xY~\xd5I\xdar\
+\x8f\x82rU\xb2\x99\xc9y\xf8\xdaU\xe1\xb6e\xb4E\
+\xbaGUN\xdc\xae\x07\xe6iy2\xdb\x92\xceY9\
+\xb5\x7f\xd5@\xb4\xc8\xe9\xe4\x97Rl\xd6\x95(X\x0b\
+U{\xe8\xec\x08\xbaJ\xd2\xba\xd1m\x98\xda\xe6k\x09\
+o\xd2\xf7\xc3L?\x22\x82C\xc7\xf7}\xe2}\xe8\x06\
+\x91s\x99\x97}\x8b\x1a\xb5/!\x09\xf0_\x99\xdd\xe7\
+\xc9\x9c\xcd\xe3\x87FI\xf1\xc4\xb2{l>\xed\xe3\x01\
+\xff\x19v\x12\xcf\xd7b\xd2\xc9\xa1{L\x96\x00P\x95\
+\x05\x9d\xac\xa0\xc0\x02R\xd2}V\xc3\xdb\xbe\x93\xb6\xcc\
+\x91\x91\x116\xd7T\xad\x02 \x84^\xa8y \x0c\x1c\
+\xcf\x03\xe8\xc5\x83T3\x12\x89\x9d \x09\xe2\x01\xc7R\
+\x0b\x9d8\xc08\x8d\xfa\xa9M\x14\xdb\xa9\xce\x02\xb0\x92\
+\x19\xba\x86L\x80\x03&\x9f\x99}Z\xbc\x00b0\xdf\
+a\xb9\x80\xa8\xf2\xc1H\x9e\xa9\xe4\xb4V#\xd9\xd6l\
+\xf1\x91\x08\x8ac*_\x8de\xb0[3\x82\x9d0\x81\
+\xde\xfb\xd8[\xef\x1eJ^\xb3\x96!2\xe2x\xe1\xc1\
+pA+^\xee\xb3w?A\xaa\xd2\xfa\xb9\xa4\xf9\xd3\
+\xbb\x07\xd4\xc1\x00\x1d\x82\xadY\xce\x17<\x87c\x8a\xa8\
+G\xa6\x1f\xad\xdfL\xd2w\xa3\x05\xa3\x92/\xa9\xda@\
+\xb1\x97\xcaA\x10\xf8\xb2\xa2\x06\x0a\x95<\x1f\xe9\x16\xcc\
+DB\x0dS\x0a\x8er\xbd\xa3\xee0\xa2\x90\xa8\xce\xe0\
+T'\xd5C\x09\x06L\x22\xddi\xb0\xcb0T\x0c/\
+\xcab,\x90\x5c\x07A\x1a\xd7Y)\x91\x9a\xb7q\xea\
+|%d\x1b\xe8\x80DJ\x03\x98\xa9y\x18`\xd3\x01\
+\xf3\x18S\x1d*\xbd(\xd0\xcd\x1e\x90\xa1\xb9\x8dxp\
+\xa4 q\x90\xf6R\x00Q\x88\x1d\x12c\x12\xc4#\x14\
+\xea\x85\xc0\x8b)\xe8\x85G;K\x0ae\xb6U\x8aa\
+\x13M\x14\x94T\x0f\x0c\xd33\xa2\x14\x1aKz\xcc\xf6\
+\xa05\xa1\xb5\x03\xc4\xf6\x8e\xe4\x97Wwe}\xff-\
+P\xff_\xb8\xfc\xf7\x98\xbb\x06\xacY=q\xcd(\xe1\
+=\xa0C\xdc\xb8\xe6\xc65\xc3n\x86]\x1b\xc5q\x1a\
+\xf9\xc7TC\x88\xe7\xa4Q\xe8\x91KT\x93\xbc\x86K\
+\xa21\x97\x9cg\xbf\x92\xffF%o\x82J\xd8\x8dJ\
+\xde\xc6L\xdf\x22\x95\x10\x12\xc0\xf1\xdd\x8f\xc2c*A\
+a\x02'b\x12\x90\xe8\x02\x97\x1c\xf9\xf77\x88\xfe\xfc\
+\x82r\xb8\xf7B\x8d\xe6FO\x12/\x89\xbd\x97\x98\xeb\
+\x13\x0a\x5c#\xd2 H\xc2\xe8\x1fNE\xaf;\x14\xf9\
+xLd\xe7\xb5_\xab\xfe\xc6do\x82\xc9v7&\
+{\x1b3}\x8bL\x86\xc2\x18\xce%$\x8c\xbc\x11\x95\
+\xe9\x8bOB\xfc$\xbcDe\xd1\x0b\xb9\x092\xfa$\
+\x0cB\x1c\xbe\x8c\xfb\x10\xcc\x1cG\x01N\xc9%2\x03\
+\xbf\x12nx\xe4U\x5c\x16\x9c\x5c\xf0\xce\xab\xbfV\xff\
+\x8d\xccndv#\xb3\xefFf\xed\x1f2_\x7f\xc3\
+\xf3\x03\xdfOF7<\xd8\xca\xbew\xfc\xb7\x9d\x81\xca\
+\x02\xfc\x1a2I\xceox\xe3\xecW\xf2\xdf\xa8\xe4;\
+S\xc9U\xf6x<g\x8f\x89\xbb\x9c\xddM\xf4\xff,\
+fw\x7f\x038y\x18\x8e\
+"
+
+qt_resource_name = b"\
+\x00\x05\
+\x00o\xa6S\
+\x00i\
+\x00c\x00o\x00n\x00s\
+\x00\x10\
+\x08\x12\xa3'\
+\x00m\
+\x00e\x00d\x00i\x00a\x00-\x00r\x00e\x00c\x00o\x00r\x00d\x00.\x00s\x00v\x00g\
+\x00\x09\
+\x0f\xd7\xb3G\
+\x00n\
+\x00e\x00x\x00x\x00T\x00.\x00s\x00v\x00g\
+"
+
+qt_resource_struct = b"\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
+\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
+\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
+\x00\x00\x01{M\xfa\x09 \
+\x00\x00\x006\x00\x01\x00\x00\x00\x01\x00\x00\x08h\
+\x00\x00\x01{M\xfa\x09!\
+"
+
+def qInitResources():
+    QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+def qCleanupResources():
+    QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+qInitResources()
diff --git a/nexxT/interface/__init__.py b/nexxT/interface/__init__.py
index 2f35ac2..6f3a06f 100644
--- a/nexxT/interface/__init__.py
+++ b/nexxT/interface/__init__.py
@@ -41,7 +41,8 @@
     from nexxT.core import PortImpl
     OutputPort = PortImpl.OutputPortImpl
     InputPort = PortImpl.InputPortImpl
-    OutputPortInterface.setupPortToPortConnection = OutputPort.setupPortToPortConnection
+    OutputPortInterface.setupDirectConnection = OutputPort.setupDirectConnection
+    OutputPortInterface.setupInterThreadConnection = OutputPort.setupInterThreadConnection
     del PortImpl
     from nexxT.interface.Filters import Filter, FilterState, FilterSurrogate
     from nexxT.interface.DataSamples import DataSample
diff --git a/nexxT/services/gui/Configuration.py b/nexxT/services/gui/Configuration.py
index e445a8a..9bfbda4 100644
--- a/nexxT/services/gui/Configuration.py
+++ b/nexxT/services/gui/Configuration.py
@@ -319,6 +319,7 @@ def _dirtyChanged(self, dirty):
             title = "nexxT: " + self.cfgfile
         if dirty:
             title += " *"
+        self.actSave.setEnabled(dirty)
         srv.setWindowTitle(title)
 
     def _onItemDoubleClicked(self, index):
@@ -391,6 +392,7 @@ def activeAppStateChange(self, newState):
         :return:
         """
         assertMainThread()
+        logger.debug("activeAppStateChange(%s)", FilterState.state2str(newState))
         if newState == FilterState.CONSTRUCTED:
             self.actActivate.setEnabled(True)
         else:
diff --git a/nexxT/services/gui/GraphEditor.py b/nexxT/services/gui/GraphEditor.py
index bbd3ea9..5874e38 100644
--- a/nexxT/services/gui/GraphEditor.py
+++ b/nexxT/services/gui/GraphEditor.py
@@ -1278,6 +1278,7 @@ def setThread(self):
         mockup = self.graph.getMockup(item.name)
         pc = mockup.propertyCollection().getChildCollection("_nexxT")
         pc.setProperty("thread", newThread)
+        self.graph.getSubConfig().getConfiguration().setDirty(True)
         item.sync()
 
     def onAddNode(self):
diff --git a/nexxT/services/gui/MainWindow.py b/nexxT/services/gui/MainWindow.py
index b58223c..96bb3ac 100644
--- a/nexxT/services/gui/MainWindow.py
+++ b/nexxT/services/gui/MainWindow.py
@@ -278,6 +278,12 @@ def __del__(self):
 
     @Slot(str, QPoint)
     def updateSelection(self, group, point):
+        """
+        QT Meta-function which can be called to update the 2D selection.
+
+        :param group: the group name given as str/QString
+        :param point: the new selection point given as QPoint
+        """
         self.userSelectionChanged.emit(group, point)
 
     @Slot()
@@ -374,8 +380,9 @@ def subplot(self, windowId, theFilter, widget):
         #       with the 100 ms timeout this couldn't be reproduced
         QTimer.singleShot(100, lambda: (
             self.managedSubplots[title]["mdiSubWindow"].adjustSize() if
-            widget.parent().size().height() < widget.minimumSizeHint().height() or
-            widget.parent().size().height() < widget.minimumSize().height() else None
+            shiboken2.isValid(widget) and ( # pylint: disable=no-member
+                widget.parent().size().height() < widget.minimumSizeHint().height() or
+                widget.parent().size().height() < widget.minimumSize().height()) else None
         ))
         self.managedSubplots[title]["plots"][row, col] = widget
 
@@ -478,7 +485,7 @@ def ensureVisible():
                 h = self.mdi.viewport().height()
                 r = QRect(x, y, w, h)
                 g = window.geometry()
-                logger.info("r=%s w=%s", r, g)
+                logger.debug("r=%s w=%s", r, g)
                 if not r.intersects(g):
                     self.mdi.horizontalScrollBar().setValue(g.x())
                     self.mdi.verticalScrollBar().setValue(g.y())
@@ -511,12 +518,13 @@ def _appActivated(self, name, app):
         else:
             self.activeApp = None
 
-    def _aboutPython(self):
+    def _aboutPython(self): # pylint: disable=no-self.use
         piplic = subprocess.check_output([sys.executable, "-m", "piplicenses", "--format=plain"],
                                          encoding="utf-8").replace("\n", "<br>").replace(" ", "&nbsp;")
         piplic = piplic.replace("<br>", "<br><br>", 1)
         msgBox = QMessageBox()
-        msgBox.setText("This program uses <b>python</b> %(version)s. The used packages are listed below." % dict(version=sys.version, table=piplic))
+        msgBox.setText("This program uses <b>python</b> %(version)s. The used packages are listed below." %
+                       dict(version=sys.version, table=piplic))
         view = QScrollArea(msgBox)
         label = QLabel("<pre>%(table)s</pre>" % dict(version=sys.version, table=piplic), msgBox)
         label.setTextInteractionFlags(Qt.TextSelectableByKeyboard|Qt.TextSelectableByMouse)
diff --git a/nexxT/src/Executor.cpp b/nexxT/src/Executor.cpp
deleted file mode 100644
index ce928fd..0000000
--- a/nexxT/src/Executor.cpp
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * SPDX-License-Identifier: Apache-2.0
- * Copyright (C) 2020 ifm electronic gmbh
- *
- * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
- */
-
-#include "Executor.hpp"
-#include "FilterEnvironment.hpp"
-#include "InputPortInterface.hpp"
-#include "OutputPortInterface.hpp"
-#include "Logger.hpp"
-
-#include <QtCore/QThread>
-#include <QtCore/Qt>
-#include <QtCore/QMutex>
-#include <QtCore/QMutexLocker>
-#include <QtCore/QSemaphore>
-#include <QtCore/QTimer>
-
-#include <vector>
-#include <set>
-#include <chrono>
-
-/* maximum number of events processed before step function returns */
-#define MAX_EVENTS_PER_STEP (32)
-/* after this time has elapsed, the step function will return to avoid unresponsiveness */
-#define STEP_DEADLINE (100ms)
-
-using namespace nexxT;
-
-namespace nexxT
-{
-    struct ExecutorD
-    {
-        struct ReceiveEvent
-        {
-            SharedInputPortPtr inputPort;
-            SharedDataSamplePtr dataSample;
-            QSemaphore *semaphore;
-        };
-
-        static const int MAX_LOOPS_FINALIZE = 5;
-
-        QMutex pendingReceivesMutex;
-        std::vector<ReceiveEvent> pendingReceives;
-        std::set<Filter *> blockedFilters;
-        bool stopped;
-        // in contrast to the python reference implementation, the pressure on the QT event loop is reduced in the C++
-        // implementation by omitting unnecessary notify calls through the event loop. We count the number of pending
-        // notify calls and omit the call if there is already a pending call in the event loop.
-        int32_t numNotifiesInQueue;
-    };
-};
-
-Executor::Executor(QThread *qthread) :
-    QObject(),
-    d(new ExecutorD())
-{
-    moveToThread(qthread);
-    QObject::connect(this, SIGNAL(notify()), this, SLOT(notifyInThread()), Qt::QueuedConnection);
-    d->numNotifiesInQueue = 0;
-}
-
-Executor::~Executor()
-{
-    delete d;
-}
-
-void Executor::registerPendingRcvSync(const SharedInputPortPtr &inputPort,
-                                      const SharedDataSamplePtr &dataSample)
-{
-    if( !d->stopped )
-    {
-        ExecutorD::ReceiveEvent ev{inputPort, dataSample, 0};
-        {
-            QMutexLocker locker(&d->pendingReceivesMutex);
-            //NEXXT_LOG_INFO(QString("add to executor queue %1/%2 (sync)").
-            //    arg(inputPort->environment()->getFullQualifiedName()).
-            //    arg(inputPort->name()));
-            d->pendingReceives.push_back(ev);
-        }
-        notifyInThread();
-    }
-}
-
-void Executor::registerPendingRcvAsync(const SharedInputPortPtr &inputPort,
-                                       const SharedDataSamplePtr &dataSample,
-                                       QSemaphore *semaphore)
-{
-    if( !d->stopped )
-    {
-        ExecutorD::ReceiveEvent ev{inputPort, dataSample, semaphore};
-        {
-            QMutexLocker locker(&d->pendingReceivesMutex);
-            //NEXXT_LOG_INFO(QString("add to executor queue %1/%2 (async)").
-            //   arg(inputPort->environment()->getFullQualifiedName()).arg(inputPort->name()));
-            d->pendingReceives.push_back(ev);
-        }
-        emit notify();
-    }
-}
-
-void Executor::notifyInThread()
-{
-    if( QThread::currentThread() != thread() )
-    {
-        NEXXT_LOG_ERROR("Executor::notifyInThread: Unexpected thread!");
-    }
-    if(d->numNotifiesInQueue == 0)
-    {
-        //NEXXT_LOG_INFO(QString("[%1] calling step via QT event.").arg(QThread::currentThread()->objectName()));
-        d->numNotifiesInQueue++;
-        QTimer::singleShot(0, this, SLOT(multiStep()));
-    }
-}
-
-void Executor::multiStep()
-{
-    using namespace std::literals::chrono_literals;
-    std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
-    std::chrono::steady_clock::time_point curr = begin;
-    uint32_t maxEvents = MAX_EVENTS_PER_STEP;
-    d->numNotifiesInQueue--;
-    if(d->numNotifiesInQueue < 0)
-    {
-        NEXXT_LOG_ERROR("Unexpected numNotifiesInQueue!");
-    }
-    while( (!d->stopped) && (maxEvents > 0) && (curr - begin < STEP_DEADLINE) )
-    {
-        if(!step())
-        {
-            /* no more events to process */
-            return;
-        } else
-        {
-            maxEvents--;
-        }
-        curr = std::chrono::steady_clock::now();
-    }
-    /* the last step() function result was true, so we aborted early, register another multiStep call */
-    notifyInThread();
-}
-
-struct StepFunctionHelper
-{
-    const SharedFilterPtr &fromFilter;
-    ExecutorD *d;
-    bool &res;
-
-    StepFunctionHelper(const SharedFilterPtr &_fromFilter, ExecutorD *_d, bool &_res) :
-        fromFilter(_fromFilter),
-        d(_d),
-        res(_res)
-    {
-        if( fromFilter.get() != 0 )
-        {
-            //NEXXT_LOG_INFO(QString("[%1] Entering Executor::step, blocking filter %2").
-            //    arg(QThread::currentThread()->objectName()).
-            //    arg(fromFilter->environment()->getFullQualifiedName()));
-            d->blockedFilters.insert(fromFilter.get());
-        } else
-        {
-            //NEXXT_LOG_INFO(QString("[%1] Entering Executor::step without blocking").
-            //    arg(QThread::currentThread()->objectName()));
-        }
-    }
-    ~StepFunctionHelper()
-    {
-        if( !res )
-        {
-            d->pendingReceivesMutex.unlock();
-        }
-        if( fromFilter.get() != 0 )
-        {
-            //NEXXT_LOG_INFO(QString("[%1] Unblocking filter %2").
-            //    arg(QThread::currentThread()->objectName()).
-            //    arg(fromFilter->environment()->getFullQualifiedName()));
-            d->blockedFilters.erase(fromFilter.get());
-        }
-        //NEXXT_LOG_INFO(QString("[%1] Leaving Executor::step").
-        //    arg(QThread::currentThread()->objectName()));
-    }
-};
-
-bool Executor::step(const SharedFilterPtr &fromFilter)
-{
-    bool res = false;
-    if( !d->stopped )
-    {
-        StepFunctionHelper helper(fromFilter, d, res);
-        d->pendingReceivesMutex.lock();
-        for(auto it=d->pendingReceives.begin(); it != d->pendingReceives.end(); it++)
-        {
-            if( d->blockedFilters.empty() ||
-                d->blockedFilters.count(it->inputPort->environment()->getPlugin().get()) == 0 )
-            {
-                ExecutorD::ReceiveEvent ev(*it);
-                d->pendingReceives.erase(it);
-                /* it is invalid from here on */
-                d->pendingReceivesMutex.unlock();
-                res = true;
-                if( !ev.semaphore )
-                {
-                    ev.inputPort->receiveSync(ev.dataSample);
-                } else
-                {
-                    ev.inputPort->receiveAsync(ev.dataSample, ev.semaphore);
-                }
-                break;
-            }
-        }
-    }
-    return res;
-}
-
-void Executor::finalize()
-{
-    std::multiset<InputPortInterface *> numCalled;
-    bool changed = true;
-    while(changed)
-    {
-        changed = false;
-        d->pendingReceivesMutex.lock();
-        for(auto it=d->pendingReceives.begin(); it != d->pendingReceives.end(); it++)
-        {
-            bool cond1 = d->blockedFilters.count(it->inputPort->environment()->getPlugin().get()) == 0;
-            bool cond2 = numCalled.count(it->inputPort.get()) < d->MAX_LOOPS_FINALIZE;
-            if(cond1 && cond2)
-            {
-                ExecutorD::ReceiveEvent ev(*it);
-                d->pendingReceives.erase(it);
-                /* it is invalid from here on */
-                d->pendingReceivesMutex.unlock();
-                changed = true;
-                numCalled.insert(ev.inputPort.get());
-                if( !ev.semaphore )
-                {
-                    ev.inputPort->receiveSync(ev.dataSample);
-                } else
-                {
-                    ev.inputPort->receiveAsync(ev.dataSample, ev.semaphore);
-                }
-                d->pendingReceivesMutex.lock();
-                break;
-            }
-        }
-        d->pendingReceivesMutex.unlock();
-    }
-}
-
-void Executor::clear()
-{
-    d->stopped = true;
-    d->pendingReceives.clear();
-    d->blockedFilters.clear();
-}
-
-SharedExecutorPtr Executor::make_shared(Executor *executor)
-{
-    return SharedExecutorPtr(executor);
-}
diff --git a/nexxT/src/Executor.hpp b/nexxT/src/Executor.hpp
deleted file mode 100644
index e664f76..0000000
--- a/nexxT/src/Executor.hpp
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * SPDX-License-Identifier: Apache-2.0
- * Copyright (C) 2020 ifm electronic gmbh
- *
- * THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
- */
-
-/**
-    \file Executor.hpp
-    This file is the c++ version of \verbatim embed:rst :py:mod:`nexxT.core.Executor` \endverbatim
-*/
-
-#ifndef NEXXT_EXECUTOR_HPP
-#define NEXXT_EXECUTOR_HPP
-
-#include <QtCore/QObject>
-
-#include "SharedPointerTypes.hpp"
-#include "Filters.hpp"
-#include "DataSamples.hpp"
-#include "NexxTLinkage.hpp"
-
-class QSemaphore;
-class QThread;
-
-//! @cond Doxygen_Suppress
-namespace nexxT
-{
-    struct ExecutorD;
-
-    /*!
-        This class is the C++ variant of \verbatim embed:rst:inline :py:class:`nexxT.core.Executor.Executor`
-        \endverbatim
-    */
-    class DLLEXPORT Executor: public QObject
-    {
-        Q_OBJECT
-
-        ExecutorD *d;
-    public:
-        /*!
-            Constructor, see \verbatim embed:rst:inline :py:meth:`nexxT.core.Executor.Executor.__init__`
-            \endverbatim
-        */
-        Executor(QThread *qthread);
-        /*!
-            Destructor
-        */
-        virtual ~Executor();
-
-        /*!
-            See \verbatim embed:rst:inline :py:meth:`nexxT.core.Executor.Executor.getContent` \endverbatim
-        */
-        void finalize();
-        /*!
-            See \verbatim embed:rst:inline :py:meth:`nexxT.core.Executor.Executor.clear` \endverbatim
-        */
-        void clear();
-
-        /*!
-            Returns a shared pointer of the given executor instance, which takes the ownership of the instance.
-        */
-        static SharedExecutorPtr make_shared(Executor *executor);
-
-    signals:
-
-        /*!
-            See \verbatim embed:rst:inline :py:meth:`nexxT.core.Executor.Executor.notify` \endverbatim
-        */
-        void notify();
-
-    protected slots:
-        /*!
-            This function processes a bunch of step() functions. It's only called via QTimer::singleShot
-        */
-        void multiStep();
-
-    public slots:
-        /*!
-            See \verbatim embed:rst:inline :py:meth:`nexxT.core.Executor.Executor.registerPendingRcvSync` \endverbatim
-        */
-        void registerPendingRcvSync(const SharedInputPortPtr &inputPort, const SharedDataSamplePtr &dataSample);
-        /*!
-            See \verbatim embed:rst:inline :py:meth:`nexxT.core.Executor.Executor.registerPendingRcvAsync` \endverbatim
-        */
-        void registerPendingRcvAsync(const SharedInputPortPtr &inputPort, const SharedDataSamplePtr &dataSample, QSemaphore *semaphore);
-        /*!
-            See \verbatim embed:rst:inline :py:meth:`nexxT.core.Executor.Executor.notifyInThread` \endverbatim
-        */
-        void notifyInThread();
-        /*!
-            See \verbatim embed:rst:inline :py:meth:`nexxT.core.Executor.Executor.step` \endverbatim
-        */
-        bool step(const SharedFilterPtr &fromFilter = SharedFilterPtr());
-    };
-};
-//! @endcond
-
-#endif
diff --git a/nexxT/src/InputPortInterface.cpp b/nexxT/src/InputPortInterface.cpp
index 49c73fd..9fd5ea5 100644
--- a/nexxT/src/InputPortInterface.cpp
+++ b/nexxT/src/InputPortInterface.cpp
@@ -183,8 +183,8 @@ void InputPortInterface::receiveAsync(const QSharedPointer<const DataSample> &sa
         addToQueue(sample);
         if(!d->interthreadDynamicQueue)
         {
-            semaphore->release(1);
             transmit();
+            semaphore->release(1);
         } else
         {
             if( d->semaphoreN.find(semaphore) == d->semaphoreN.end() )
diff --git a/nexxT/src/OutputPortInterface.cpp b/nexxT/src/OutputPortInterface.cpp
index f805981..fa48512 100644
--- a/nexxT/src/OutputPortInterface.cpp
+++ b/nexxT/src/OutputPortInterface.cpp
@@ -12,7 +12,6 @@
 #include "Filters.hpp"
 #include "Logger.hpp"
 #include "Services.hpp"
-#include "Executor.hpp"
 #include <atomic>
 
 #include <QtCore/QThread>
@@ -40,25 +39,23 @@ SharedPortPtr OutputPortInterface::clone(BaseFilterEnvironment *env) const
     return SharedPortPtr(new OutputPortInterface(dynamic(), name(), env));
 }
 
-QObject *OutputPortInterface::setupPortToPortConnection(const SharedExecutorPtr &executorFrom,
-                                                        const SharedExecutorPtr &executorTo,
-                                                        const SharedPortPtr &_outputPort,
-                                                        const SharedPortPtr &_inputPort)
+void OutputPortInterface::setupDirectConnection(const SharedPortPtr &op, const SharedPortPtr &ip)
 {
-    SharedOutputPortPtr outputPort = _outputPort.dynamicCast<OutputPortInterface>();
-    SharedInputPortPtr inputPort = _inputPort.dynamicCast<InputPortInterface>();
-    PortToPortConnection *p2pc = new PortToPortConnection(executorFrom, executorTo, outputPort, inputPort);
-    if( outputPort->thread() != executorFrom->thread() )
-    {
-        NEXXT_LOG_ERROR("Unexpected threads (outputPort vs executorFrom)");
-    }
-    if( inputPort->thread() != executorTo->thread() )
-    {
-        NEXXT_LOG_ERROR("Unexpected threads (inputPort vs executorTo)");
-    }
-    const OutputPortInterface *p0 = dynamic_cast<const OutputPortInterface *>(outputPort.data());
+    const OutputPortInterface *p0 = dynamic_cast<const OutputPortInterface *>(op.data());
+    const InputPortInterface *p1 = dynamic_cast<const InputPortInterface *>(ip.data());
     QObject::connect(p0, SIGNAL(transmitSample(const QSharedPointer<const nexxT::DataSample>&)),
-                     p2pc, SLOT(receiveSample(const QSharedPointer<const nexxT::DataSample>&)),
-                     Qt::DirectConnection);
-    return p2pc;
+                     p1, SLOT(receiveSync(const QSharedPointer<const nexxT::DataSample> &)));
 }
+
+QObject *OutputPortInterface::setupInterThreadConnection(const SharedPortPtr &op, const SharedPortPtr &ip, QThread &outputThread)
+{
+    InterThreadConnection *itc = new InterThreadConnection(&outputThread);
+    const OutputPortInterface *p0 = dynamic_cast<const OutputPortInterface *>(op.data());
+    const InputPortInterface *p1 = dynamic_cast<const InputPortInterface *>(ip.data());
+    QObject::connect(p0, SIGNAL(transmitSample(const QSharedPointer<const nexxT::DataSample>&)),
+                     itc, SLOT(receiveSample(const QSharedPointer<const nexxT::DataSample>&)));
+    QObject::connect(itc, SIGNAL(transmitInterThread(const QSharedPointer<const nexxT::DataSample> &, QSemaphore *)),
+                     p1, SLOT(receiveAsync(const QSharedPointer<const nexxT::DataSample> &, QSemaphore *)));
+    return itc;
+}
+
diff --git a/nexxT/src/OutputPortInterface.hpp b/nexxT/src/OutputPortInterface.hpp
index 5c718ce..4334d2b 100644
--- a/nexxT/src/OutputPortInterface.hpp
+++ b/nexxT/src/OutputPortInterface.hpp
@@ -65,10 +65,11 @@ namespace nexxT
         /*!
             Called by the nexxT framework, not intended to be used directly.
         */
-        static QObject *setupPortToPortConnection(const SharedExecutorPtr &executorFrom,
-                                                  const SharedExecutorPtr &executorTo,
-                                                  const SharedPortPtr &portFrom,
-                                                  const SharedPortPtr &portTo);
+        static void setupDirectConnection(const SharedPortPtr &, const SharedPortPtr &);
+        /*!
+            Called by the nexxT framework, not intended to be used directly.
+        */
+        static QObject *setupInterThreadConnection(const SharedPortPtr &, const SharedPortPtr &, QThread &);
     };
 
 };
diff --git a/nexxT/src/Ports.cpp b/nexxT/src/Ports.cpp
index a591f40..690f529 100644
--- a/nexxT/src/Ports.cpp
+++ b/nexxT/src/Ports.cpp
@@ -11,7 +11,6 @@
 #include "Filters.hpp"
 #include "Logger.hpp"
 #include "Services.hpp"
-#include "Executor.hpp"
 #include "OutputPortInterface.hpp"
 #include "InputPortInterface.hpp"
 #include <atomic>
@@ -31,26 +30,11 @@ namespace nexxT
         BaseFilterEnvironment *environment;
     };
 
-    struct PortToPortConnectionD
+    struct InterThreadConnectionD
     {
         QSemaphore semaphore;
         std::atomic_bool stopped;
-        SharedExecutorPtr executorFrom;
-        SharedExecutorPtr executorTo;
-        SharedOutputPortPtr portFrom;
-        SharedInputPortPtr portTo;
-        PortToPortConnectionD(int n,
-                               const SharedExecutorPtr &_executorFrom,
-                               const SharedExecutorPtr &_executorTo,
-                               const SharedOutputPortPtr &_portFrom,
-                               const SharedInputPortPtr &_portTo)
-            : semaphore(n)
-            , stopped(true)
-            , executorFrom(_executorFrom)
-            , executorTo(_executorTo)
-            , portFrom(_portFrom)
-            , portTo(_portTo)
-            {}
+        InterThreadConnectionD(int n) : semaphore(n), stopped(true) {}
     };
 
 };
@@ -102,53 +86,35 @@ SharedPortPtr Port::make_shared(Port *port)
     return SharedPortPtr(port);
 }
 
-PortToPortConnection::PortToPortConnection(const SharedExecutorPtr &executorFrom,
-                                           const SharedExecutorPtr &executorTo,
-                                           const SharedOutputPortPtr &portFrom,
-                                           const SharedInputPortPtr &portTo)
-    : d(new PortToPortConnectionD(1, executorFrom, executorTo, portFrom, portTo))
+InterThreadConnection::InterThreadConnection(QThread *from_thread)
+    : d(new InterThreadConnectionD(1))
 {
+    moveToThread(from_thread);
 }
 
-PortToPortConnection::~PortToPortConnection()
+InterThreadConnection::~InterThreadConnection()
 {
     delete d;
 }
 
-void PortToPortConnection::receiveSample(const QSharedPointer<const DataSample> &sample)
+void InterThreadConnection::receiveSample(const QSharedPointer<const DataSample> &sample)
 {
-    if( d->executorFrom.get() == d->executorTo.get() )
+    while(true)
     {
-        d->executorTo->registerPendingRcvSync(d->portTo, sample);
-    } else
-    {
-        int32_t timeoutMS = 0;
-        while(true)
+        if( d->stopped.load() )
+        {
+            NEXXT_LOG_WARN("The inter-thread connection is set to stopped mode; data sample discarded.");
+            break;
+        }
+        if( d->semaphore.tryAcquire(1, 500) )
         {
-            if( d->stopped.load() )
-            {
-                NEXXT_LOG_WARN("The inter-thread connection is set to stopped mode; data sample discarded.");
-                break;
-            }
-            if( !d->semaphore.tryAcquire(1, timeoutMS) )
-            {
-                if( d->executorFrom->step(d->portFrom->environment()->getPlugin()) )
-                {
-                    timeoutMS = 0;
-                } else
-                {
-                    timeoutMS = 10;
-                }
-            } else
-            {
-                d->executorTo->registerPendingRcvAsync(d->portTo, sample, &d->semaphore);
-                break;
-            }
+            emit transmitInterThread(sample, &d->semaphore);
+            break;
         }
     }
 }
 
-void PortToPortConnection::setStopped(bool stopped)
+void InterThreadConnection::setStopped(bool stopped)
 {
     d->stopped.store(stopped);
-}
\ No newline at end of file
+}
diff --git a/nexxT/src/Ports.hpp b/nexxT/src/Ports.hpp
index 22f5cf1..49c5068 100644
--- a/nexxT/src/Ports.hpp
+++ b/nexxT/src/Ports.hpp
@@ -22,7 +22,7 @@ namespace nexxT
 {
     class BaseFilterEnvironment;
     struct PortD;
-    struct PortToPortConnectionD;
+    struct InterThreadConnectionD;
 
     /*!
         This class is the C++ variant of \verbatim embed:rst:inline :py:class:`nexxT.interface.Ports.Port`
@@ -83,17 +83,17 @@ namespace nexxT
     };
 
  //! @cond Doxygen_Suppress
-   class DLLEXPORT PortToPortConnection : public QObject
+   class DLLEXPORT InterThreadConnection : public QObject
     {
         Q_OBJECT
-        
-        PortToPortConnectionD *const d;
+
+        InterThreadConnectionD *const d;
     public:
-        PortToPortConnection(const SharedExecutorPtr &executorFrom,
-                              const SharedExecutorPtr &executorTo,
-                              const SharedOutputPortPtr &portFrom,
-                              const SharedInputPortPtr &portTo);
-        virtual ~PortToPortConnection();
+        InterThreadConnection(QThread *qthread_from);
+        virtual ~InterThreadConnection();
+
+    signals:
+        void transmitInterThread(const QSharedPointer<const nexxT::DataSample> &sample, QSemaphore *semaphore);
 
     public slots:
         void receiveSample(const QSharedPointer<const nexxT::DataSample> &sample);
diff --git a/nexxT/src/SConscript.py b/nexxT/src/SConscript.py
index 4febeae..ebf911f 100644
--- a/nexxT/src/SConscript.py
+++ b/nexxT/src/SConscript.py
@@ -56,7 +56,6 @@
     Services.cpp
     PropertyCollection.cpp
     NexxTPlugins.cpp
-    Executor.cpp
 """)), CPPDEFINES=["NEXXT_LIBRARY_COMPILATION"])
 env.RegisterTargets(apilib)
 
@@ -65,7 +64,7 @@
 targets += [spath.Dir("cnexxT").File("cnexxt_module_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("nexxt_datasample_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("nexxt_port_wrapper.cpp")]
-targets += [spath.Dir("cnexxT").File("nexxt_porttoportconnection_wrapper.cpp")]
+targets += [spath.Dir("cnexxT").File("nexxt_interthreadconnection_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("nexxt_outputportinterface_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("nexxt_inputportinterface_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("nexxt_services_wrapper.cpp")]
@@ -77,14 +76,10 @@
 targets += [spath.Dir("cnexxT").File("nexxt_basefilterenvironment_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("nexxt_plugininterface_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("nexxt_logging_wrapper.cpp")]
-targets += [spath.Dir("cnexxT").File("nexxt_executor_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("qsharedpointer_datasample_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("qsharedpointer_filter_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("qsharedpointer_port_wrapper.cpp")]
 targets += [spath.Dir("cnexxT").File("qsharedpointer_qobject_wrapper.cpp")]
-targets += [spath.Dir("cnexxT").File("qsharedpointer_executor_wrapper.cpp")]
-targets += [spath.Dir("cnexxT").File("qsharedpointer_inputportinterface_wrapper.cpp")]
-targets += [spath.Dir("cnexxT").File("qsharedpointer_outputportinterface_wrapper.cpp")]
 
 
 env = env.Clone()
diff --git a/nexxT/src/Services.cpp b/nexxT/src/Services.cpp
index dd0e840..4976015 100644
--- a/nexxT/src/Services.cpp
+++ b/nexxT/src/Services.cpp
@@ -61,8 +61,14 @@ void Services::_addService(const QString &name, const SharedQObjectPtr &service)
         NEXXT_LOG_WARN(QString("Service %1 already existing; automatically removing it.").arg(name));
         removeService(name);
     }
-    NEXXT_LOG_INFO(QString("adding service %1").arg(name));
-    d->map[name] = service;
+    if( !service )
+    {
+        NEXXT_LOG_WARN(QString("Given service %1 is NULL. No service added.").arg(name));
+    } else
+    {
+        NEXXT_LOG_INFO(QString("adding service %1").arg(name));
+        d->map[name] = service;
+    }
 }
 
 void Services::_removeService(const QString &name)
diff --git a/nexxT/src/SharedPointerTypes.hpp b/nexxT/src/SharedPointerTypes.hpp
index 73749c8..8720d35 100644
--- a/nexxT/src/SharedPointerTypes.hpp
+++ b/nexxT/src/SharedPointerTypes.hpp
@@ -8,7 +8,6 @@ namespace nexxT
     class Port;
     class InputPortInterface;
     class OutputPortInterface;
-    class Executor;
     class DataSample;
 
     /*!
@@ -31,10 +30,6 @@ namespace nexxT
     */
     typedef QSharedPointer<OutputPortInterface> SharedOutputPortPtr;
 
-    /*!
-        A typedef for a Port instance handled by a shared pointer.
-    */
-    typedef QSharedPointer<Executor> SharedExecutorPtr;
     /*!
         A typedef for a list of ports.
     */
diff --git a/nexxT/src/cnexxT.h b/nexxT/src/cnexxT.h
index 56c3887..fddad59 100644
--- a/nexxT/src/cnexxT.h
+++ b/nexxT/src/cnexxT.h
@@ -17,4 +17,4 @@
 #include "PropertyCollection.hpp"
 #include "NexxTPlugins.hpp"
 #include "Logger.hpp"
-#include "Executor.hpp"
+#include "SharedPointerTypes.hpp"
diff --git a/nexxT/src/cnexxT.xml b/nexxT/src/cnexxT.xml
index 66a8e2f..41f9500 100644
--- a/nexxT/src/cnexxT.xml
+++ b/nexxT/src/cnexxT.xml
@@ -33,7 +33,7 @@
             </modify-function>            
         </object-type>
         
-        <object-type name="PortToPortConnection" allow-thread="true">
+        <object-type name="InterThreadConnection" allow-thread="true">
         </object-type>
         
         <object-type name="Services">
@@ -134,18 +134,5 @@
 
         <object-type name="Logging" />
 
-        <object-type name="Executor">
-            <modify-function signature="make_shared(nexxT::Executor*)">
-                <modify-argument index="1">
-                    <define-ownership owner="c++"/>
-                </modify-argument>
-            </modify-function>
-            <modify-function signature="registerPendingRcvAsync(const QSharedPointer&lt;nexxT::InputPortInterface&gt; &amp;, const QSharedPointer&lt;const nexxT::DataSample&gt; &amp;, QSemaphore *)">
-                <modify-argument index="3">
-                    <define-ownership owner="c++"/>
-                </modify-argument>
-            </modify-function>
-        </object-type>
-
     </namespace-type>
 </typesystem>


Test patch:
diff --git a/nexxT/tests/core/test_ActiveApplication.py b/nexxT/tests/core/test_ActiveApplication.py
index 18951cf..e5486b8 100644
--- a/nexxT/tests/core/test_ActiveApplication.py
+++ b/nexxT/tests/core/test_ActiveApplication.py
@@ -132,16 +132,10 @@ def test_multiThreadSimple():
     events = simple_setup(multithread=True, sourceFreq=4.0, sinkTime=0.5, activeTime_s=2, dynamicFilter=False)
     t_transmit_source = [e["time"] for e in events if e["object"] == "SimpleSource" and e["function"] == "afterTransmit"]
     t_receive_sink = [e["time"] for e in events if e["object"] == "SimpleStaticFilter" and e["function"] == "afterReceive"]
+    print("t_transmit_source=%s" % [t - t_transmit_source[0] for t in t_transmit_source])
+    print("t_receive_sink=%s" % [t - t_transmit_source[0] for t in t_receive_sink])
     try:
-        # t = 0.00: the sink takes the data and transmit returns instantly -> second transmit is with sourceFreq framerate
-        # t = 0.25: the inter thread connection buffers the data (while the sink computes) and transmit returns instantly
-        assert t_transmit_source[1] - t_transmit_source[0] < 0.3
-        # t = 0.50: the sink computation is done, and the sink gets the second data while the semaphore is released
-        # t = 0.50: the inter thread connection buffers the data (while the sink computes) and transmit returns instantly
-        assert t_transmit_source[2] - t_transmit_source[1] < 0.3
-        # t = 0.75: the source's transmit function blocks at the semaphore
-        # t = 1.00: the sink computation of second data is done, and the sink gets the third data while the semaphore is released
-        assert all([t_transmit_source[i] - t_transmit_source[i-1] > 0.4 and t_transmit_source[i] - t_transmit_source[i-1] < 0.6 for i in range(3, len(t_transmit_source))])
+        assert all([t_transmit_source[i] - t_transmit_source[i-1] > 0.4 and t_transmit_source[i] - t_transmit_source[i-1] < 0.6 for i in range(1, len(t_receive_sink))])
         # t = 1.00: the source's transmit function returns
         # t = 1.00: new data at source arrived already, the source's transmit function blocks at the semaphore
         # t = 1.50: the sink computation of third data is done, and the sink gets the fourth data while the semaphore is released
diff --git a/nexxT/tests/integration/latency.json b/nexxT/tests/integration/latency.json
new file mode 100644
index 0000000..367f6cf
--- /dev/null
+++ b/nexxT/tests/integration/latency.json
@@ -0,0 +1,124 @@
+{
+  "_guiState": {
+    "PlaybackControl_showAllFiles": 0
+  },
+  "composite_filters": [],
+  "applications": [
+    {
+      "name": "test_latency",
+      "_guiState": {},
+      "nodes": [
+        {
+          "name": "PySimpleSource",
+          "library": "entry_point://tests.nexxT.PySimpleSource",
+          "factoryFunction": "PySimpleSource",
+          "dynamicInputPorts": [],
+          "staticInputPorts": [],
+          "dynamicOutputPorts": [],
+          "staticOutputPorts": [
+            "outPort"
+          ],
+          "thread": "source",
+          "properties": {
+            "frequency": 10.0,
+            "log_tr": true
+          }
+        },
+        {
+          "name": "PySimpleStaticFilter",
+          "library": "entry_point://tests.nexxT.PySimpleStaticFilter",
+          "factoryFunction": "PySimpleStaticFilter",
+          "dynamicInputPorts": [],
+          "staticInputPorts": [
+            "inPort"
+          ],
+          "dynamicOutputPorts": [],
+          "staticOutputPorts": [
+            "outPort"
+          ],
+          "thread": "main",
+          "properties": {
+            "an_enum_property": "e1",
+            "an_int_property": 4223,
+            "log_prefix": "filter1:",
+            "log_rcv": true,
+            "log_throughput_at_end": false,
+            "sleep_time": 1.0
+          }
+        },
+        {
+          "name": "PySimpleStaticFilter2",
+          "library": "entry_point://tests.nexxT.PySimpleStaticFilter",
+          "factoryFunction": "PySimpleStaticFilter",
+          "dynamicInputPorts": [],
+          "staticInputPorts": [
+            "inPort"
+          ],
+          "dynamicOutputPorts": [],
+          "staticOutputPorts": [
+            "outPort"
+          ],
+          "thread": "main",
+          "properties": {
+            "an_enum_property": "e1",
+            "an_int_property": 4223,
+            "log_prefix": "filter2:",
+            "log_rcv": true,
+            "log_throughput_at_end": false,
+            "sleep_time": 1.0
+          }
+        },
+        {
+          "name": "PySimpleStaticFilter3",
+          "library": "entry_point://tests.nexxT.PySimpleStaticFilter",
+          "factoryFunction": "PySimpleStaticFilter",
+          "dynamicInputPorts": [],
+          "staticInputPorts": [
+            "inPort"
+          ],
+          "dynamicOutputPorts": [],
+          "staticOutputPorts": [
+            "outPort"
+          ],
+          "thread": "main",
+          "properties": {
+            "an_enum_property": "e1",
+            "an_int_property": 4223,
+            "log_prefix": "filter3:",
+            "log_rcv": true,
+            "log_throughput_at_end": false,
+            "sleep_time": 1.0
+          }
+        },
+        {
+          "name": "PySimpleStaticFilter4",
+          "library": "entry_point://tests.nexxT.PySimpleStaticFilter",
+          "factoryFunction": "PySimpleStaticFilter",
+          "dynamicInputPorts": [],
+          "staticInputPorts": [
+            "inPort"
+          ],
+          "dynamicOutputPorts": [],
+          "staticOutputPorts": [
+            "outPort"
+          ],
+          "thread": "main",
+          "properties": {
+            "an_enum_property": "e1",
+            "an_int_property": 4223,
+            "log_prefix": "filter4:",
+            "log_rcv": true,
+            "log_throughput_at_end": false,
+            "sleep_time": 0.0
+          }
+        }
+      ],
+      "connections": [
+        "PySimpleSource.outPort -> PySimpleStaticFilter.inPort",
+        "PySimpleStaticFilter.outPort -> PySimpleStaticFilter2.inPort",
+        "PySimpleStaticFilter2.outPort -> PySimpleStaticFilter3.inPort",
+        "PySimpleStaticFilter3.outPort -> PySimpleStaticFilter4.inPort"
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/nexxT/tests/integration/test_Latency.py b/nexxT/tests/integration/test_Latency.py
new file mode 100644
index 0000000..f348cd8
--- /dev/null
+++ b/nexxT/tests/integration/test_Latency.py
@@ -0,0 +1,82 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright (C) 2020 ifm electronic gmbh
+#
+# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
+#
+
+import datetime
+from pathlib import Path
+import re
+import subprocess
+import sys
+
+def parse_log(strlog):
+    res = []
+    for l in strlog.split("\n"):
+        l = l.strip()
+        if l == "":
+            continue
+        try:
+            t = datetime.datetime.strptime(l[:19], '%Y-%m-%d %H:%M:%S')
+            ms = int(l[20:23])
+            t = t + datetime.timedelta(microseconds=ms*1000)
+            l = l[24:]
+            level = l[:l.find(" ")]
+            l = l[l.find(" ")+1:]
+            module = l[:l.find(":")]
+            l = l[l.find(":")+1:]
+            msg = l.strip()
+            res.append((t,level,module,msg))
+        except:
+            pass
+    return res
+
+def test_latency():
+    try:
+        p = subprocess.run(
+            [sys.executable, "-m", "nexxT.core.AppConsole", "--gui", "false", "-a", "test_latency", "-e", """\
+from PySide2.QtCore import QTimer, QObject, QCoreApplication
+from nexxT.core.Application import Application
+from nexxT.interface import Services, FilterState 
+conf = Services.getService("Configuration")
+def stateChanged(newState):
+    if newState == FilterState.CONSTRUCTED:
+        QCoreApplication.quit()
+Application.activeApplication.stateChanged.connect(stateChanged)
+QTimer.singleShot(10000, conf.deactivate)
+""", str(Path(__file__).parent / "latency.json")
+             ],
+             capture_output=True, timeout=30., encoding="utf-8")
+        timeout = False
+    except subprocess.TimeoutExpired as e:
+        p = e
+        timeout = True
+    print("STDOUT", p.stdout)
+    print("STDERR", p.stderr)
+    assert not timeout
+    p.check_returncode()
+    assert p.stdout.strip() == ""
+    logs = parse_log(p.stderr)
+    samples = {}
+    for t, level, module, msg in logs:
+        M = re.search(r"transmit: Sample (\d+)", msg)
+        if M is not None:
+            s = int(M.group(1))
+            assert not s in samples
+            samples[s] = [t]
+        for fi in range(1,5):
+            M = re.search(r"filter%d:received: Sample (\d+)" % fi, msg)
+            if M is not None:
+                s = int(M.group(1))
+                assert s in samples and len(samples[s]) == fi
+                samples[s].append(t)
+                assert s + 1 not in samples or len(samples[s + 1]) == 1
+                assert s + 2 not in samples or len(samples[s + 2]) == 1
+
+    for s in samples:
+        ts = samples[s]
+        assert len(ts) in [1,5]
+        if len(ts) > 1:
+            latency = (ts[-1] - ts[0]).total_seconds() + 1.0 # this is the last filter's processing time
+            print("Latency of Sample %d: %.1f" % (s, latency))
+            assert latency <= 8.2
diff --git a/nexxT/tests/integration/test_gui.py b/nexxT/tests/integration/test_gui.py
index 86adbca..71ba48d 100644
--- a/nexxT/tests/integration/test_gui.py
+++ b/nexxT/tests/integration/test_gui.py
@@ -354,6 +354,19 @@ def getCurrentFrameIdx(log):
             if "received: Sample" in lastmsg:
                 return int(lastmsg.strip().split(" ")[-1])
 
+    @staticmethod
+    def assertLogItem(log, expectedLevel, expectedMsg):
+        found = False
+        model = log.logWidget.model()
+        numRows = model.rowCount(QModelIndex())
+        for row in range(numRows-1,0,-1):
+            level = model.data(model.index(row, 1, QModelIndex()), Qt.DisplayRole)
+            msg = model.data(model.index(row, 2, QModelIndex()), Qt.DisplayRole)
+            if level == expectedLevel and msg in expectedMsg:
+                found = True
+        if not found:
+            raise RuntimeError("expected message %s:%s not found in log", expectedLevel, expectedMsg)
+
     @staticmethod
     def noWarningsInLog(log, ignore=[]):
         """
@@ -1097,13 +1110,10 @@ def _stage0(self):
 
             appidx = conf.model.indexOfSubConfig(conf.configuration().applicationByName("deadlock"))
             self.cmContextMenu(conf, appidx, CM_INIT_APP)
-            self.qtbot.wait(5000)
-            # deinitialize
-            self.qtbot.keyClick(self.aw(), Qt.Key_D, Qt.ControlModifier, delay=self.delay)
-            self.noWarningsInLog(log, ignore=[
-                "did not find a playback device taking control",
-                "The inter-thread connection is set to stopped mode; data sample discarded."])
-
+            self.qtbot.wait(1000)
+            logMsg = "This graph is not deadlock-safe. A cycle has been found in the thread graph: main->compute->main"
+            self.noWarningsInLog(log, ignore=[logMsg])
+            self.assertLogItem(log, "ERROR", logMsg)
             # assert that the samples arrived in the correct order
             def assertSampleOrder():
                 numRows = log.logWidget.model().rowCount(QModelIndex())
@@ -1119,6 +1129,8 @@ def assertSampleOrder():
                         else:
                             assert idx == filters[flt] + 1
                         filters[flt] = idx
+            # at the moment we do not let the user start these configs, but the sample order is still ok with no samples
+            # at all
             assertSampleOrder()
             logger.info("finishing")
         finally:
@@ -1134,9 +1146,9 @@ def test(self):
 
 @pytest.mark.gui
 @pytest.mark.parametrize("delay", [300])
+@pytest.mark.timeout(60, method="thread")
 def test_deadlock_issue25(qtbot, xvfb, keep_open, delay, tmpdir):
     test = DeadlockTestIssue25(qtbot, xvfb, keep_open, delay, tmpdir)
-
     test.test()
 
 class ExecutionOrderTest(GuiTestBase):
@@ -1163,8 +1175,8 @@ def _stage0(self):
 
             model = log.logWidget.model()
             numRows = model.rowCount(QModelIndex())
-            # note: we changed the execution order to breadth first while fixing issue_25
-            expected = [(1,1), (1,2), (2,1), (2,2), (2,3), (2,4)]
+            # depth first execution order
+            expected = [(1,1), (2,1), (2,2), (1,2), (2,3), (2,4)]
             order = []
             for row in range(numRows):
                 msg = model.data(model.index(row, 2, QModelIndex()), Qt.DisplayRole)
diff --git a/nexxT/tests/pytest.ini b/nexxT/tests/pytest.ini
index d4f886a..a3f4d51 100644
--- a/nexxT/tests/pytest.ini
+++ b/nexxT/tests/pytest.ini
@@ -1,5 +1,6 @@
 [pytest]
 xvfb_width=1920
 xvfb_height=1080
+timeout = 600
 markers =
     gui: marks tests which are run in gui mode (deselect with '-m "not gui"')