Added new debug process separating non-wx thread extracting debug values from connector and 10 Hz wx timer refreshing Beremiz debug Viewers and communicating throw double-buffering, to avoid segmentation faults and optimize CPU usage
authorLaurent Bessard
Mon, 14 Oct 2013 10:31:32 +0200
changeset 1363 e87e0166d0a7
parent 1362 077bcba2d485
child 1364 e9e17d3b2849
Added new debug process separating non-wx thread extracting debug values from connector and 10 Hz wx timer refreshing Beremiz debug Viewers and communicating throw double-buffering, to avoid segmentation faults and optimize CPU usage
ProjectController.py
controls/DebugVariablePanel/DebugVariableGraphicPanel.py
controls/DebugVariablePanel/DebugVariableItem.py
editors/DebugViewer.py
editors/Viewer.py
graphics/DebugDataConsumer.py
--- a/ProjectController.py	Sat Oct 12 10:10:30 2013 +0900
+++ b/ProjectController.py	Mon Oct 14 10:31:32 2013 +0200
@@ -22,7 +22,7 @@
 from editors.FileManagementPanel import FileManagementPanel
 from editors.ProjectNodeEditor import ProjectNodeEditor
 from editors.IECCodeViewer import IECCodeViewer
-from editors.DebugViewer import DebugViewer
+from editors.DebugViewer import DebugViewer, REFRESH_PERIOD
 from dialogs import DiscoveryDialog
 from PLCControler import PLCControler
 from plcopen.structures import IEC_KEYWORDS
@@ -110,6 +110,9 @@
         self.MandatoryParams = None
         self._builder = None
         self._connector = None
+        self.DispatchDebugValuesTimer = None
+        self.DebugValuesBuffers = []
+        self.DebugTicks = []
         self.SetAppFrame(frame, logger)
         
         self.iec2c_path = os.path.join(base_folder, "matiec", "iec2c"+(".exe" if wx.Platform == '__WXMSW__' else ""))
@@ -157,15 +160,23 @@
         self.AppFrame = frame
         self.logger = logger
         self.StatusTimer = None
+        if self.DispatchDebugValuesTimer is not None:
+            self.DispatchDebugValuesTimer.Stop()
+        self.DispatchDebugValuesTimer = None
         
         if frame is not None:
             frame.LogViewer.SetLogSource(self._connector)
             
             # Timer to pull PLC status
-            ID_STATUSTIMER = wx.NewId()
-            self.StatusTimer = wx.Timer(self.AppFrame, ID_STATUSTIMER)
-            self.AppFrame.Bind(wx.EVT_TIMER, self.PullPLCStatusProc, self.StatusTimer)
-        
+            self.StatusTimer = wx.Timer(self.AppFrame, -1)
+            self.AppFrame.Bind(wx.EVT_TIMER, 
+                self.PullPLCStatusProc, self.StatusTimer)
+            
+            # Timer to dispatch debug values to consumers
+            self.DispatchDebugValuesTimer = wx.Timer(self.AppFrame, -1)
+            self.AppFrame.Bind(wx.EVT_TIMER, 
+                self.DispatchDebugValuesProc, self.DispatchDebugValuesTimer)
+            
             self.RefreshConfNodesBlockLists()
 
     def ResetAppFrame(self, logger):
@@ -1177,6 +1188,12 @@
     def PullPLCStatusProc(self, event):
         self.UpdateMethodsFromPLCStatus()
         
+    def SnapshotAndResetDebugValuesBuffers(self):
+        buffers, self.DebugValuesBuffers = (self.DebugValuesBuffers, 
+            [list() for iec_path in self.TracedIECPath])
+        ticks, self.DebugTicks = self.DebugTicks, []
+        return ticks, buffers
+    
     def RegisterDebugVarToConnector(self):
         self.DebugTimer=None
         Idxs = []
@@ -1210,11 +1227,12 @@
             else:
                 self.TracedIECPath = []
                 self._connector.SetTraceVariablesList([])
+            self.SnapshotAndResetDebugValuesBuffers()
             self.IECdebug_lock.release()
     
     def IsPLCStarted(self):
         return self.previous_plcstate == "Started"
-     
+    
     def ReArmDebugRegisterTimer(self):
         if self.DebugTimer is not None:
             self.DebugTimer.cancel()
@@ -1233,7 +1251,7 @@
         Idx, IEC_Type = self._IECPathToIdx.get(IECPath,(None,None))
         return IEC_Type
         
-    def SubscribeDebugIECVariable(self, IECPath, callableobj, *args, **kwargs):
+    def SubscribeDebugIECVariable(self, IECPath, callableobj, buffer_list=False, *args, **kwargs):
         """
         Dispatching use a dictionnary linking IEC variable paths
         to a WeakKeyDictionary linking 
@@ -1250,7 +1268,8 @@
                     WeakKeyDictionary(), # Callables
                     [],                  # Data storage [(tick, data),...]
                     "Registered",        # Variable status
-                    None]                # Forced value
+                    None,
+                    buffer_list]                # Forced value
             self.IECdebug_datas[IECPath] = IECdebug_data
         
         IECdebug_data[0][callableobj]=(args, kwargs)
@@ -1347,14 +1366,14 @@
             #print [dict.keys() for IECPath, (dict, log, status, fvalue) in self.IECdebug_datas.items()]
             if plc_status == "Started":
                 self.IECdebug_lock.acquire()
-                if len(debug_vars) == len(self.TracedIECPath):
+                if len(debug_vars) == len(self.DebugValuesBuffers):
                     if debug_getvar_retry > DEBUG_RETRIES_WARN:
                         self.logger.write(_("... debugger recovered\n"))
                     debug_getvar_retry = 0
-                    for IECPath,value in zip(self.TracedIECPath, debug_vars):
+                    for values_buffer, value in zip(self.DebugValuesBuffers, debug_vars):
                         if value is not None:
-                            self.CallWeakcallables(IECPath, "NewValue", debug_tick, value)
-                    self.CallWeakcallables("__tick__", "NewDataAvailable", debug_tick)
+                            values_buffer.append(value)
+                    self.DebugTicks.append(debug_tick)
                 self.IECdebug_lock.release()
                 if debug_getvar_retry == DEBUG_RETRIES_WARN:
                     self.logger.write(_("Waiting debugger to recover...\n"))
@@ -1368,6 +1387,20 @@
                 self.debug_break = True
         self.logger.write(_("Debugger disabled\n"))
         self.DebugThread = None
+        if self.DispatchDebugValuesTimer is not None:
+            self.DispatchDebugValuesTimer.Stop()
+
+    def DispatchDebugValuesProc(self, event):
+        self.IECdebug_lock.acquire()
+        debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers()
+        self.IECdebug_lock.release()
+        if len(self.TracedIECPath) == len(buffers):
+            for IECPath, values in zip(self.TracedIECPath, buffers):
+                if len(values) > 0:
+                    self.CallWeakcallables(IECPath, "NewValues", debug_ticks, values)
+            if len(debug_ticks) > 0:
+                self.CallWeakcallables("__tick__", "NewDataAvailable", debug_ticks)
+        event.Skip()
 
     def KillDebugThread(self):
         tmp_debugthread = self.DebugThread
@@ -1380,12 +1413,16 @@
             else:
                 self.logger.write(_("Debugger stopped.\n"))
         self.DebugThread = None
+        if self.DispatchDebugValuesTimer is not None:
+            self.DispatchDebugValuesTimer.Stop()
 
     def _connect_debug(self): 
         self.previous_plcstate = None
         if self.AppFrame:
             self.AppFrame.ResetGraphicViewers()
         self.RegisterDebugVarToConnector()
+        if self.DispatchDebugValuesTimer is not None:
+            self.DispatchDebugValuesTimer.Start(int(REFRESH_PERIOD * 1000))
         if self.DebugThread is None:
             self.DebugThread = Thread(target=self.DebugThreadProc)
             self.DebugThread.start()
--- a/controls/DebugVariablePanel/DebugVariableGraphicPanel.py	Sat Oct 12 10:10:30 2013 +0900
+++ b/controls/DebugVariablePanel/DebugVariableGraphicPanel.py	Mon Oct 14 10:31:32 2013 +0200
@@ -330,7 +330,7 @@
         
         DebugViewer.RefreshNewData(self, *args, **kwargs)
     
-    def NewDataAvailable(self, tick, *args, **kwargs):
+    def NewDataAvailable(self, ticks, *args, **kwargs):
         """
         Called by DataProducer for each tick captured or by panel to refresh
         graphs
@@ -338,15 +338,15 @@
         All other parameters are passed to refresh function 
         """
         # If tick given
-        if tick is not None:
-            self.HasNewData = True
+        if ticks is not None:
+            tick = ticks[-1]
             
             # Save tick as start tick for range if data is still empty
             if len(self.Ticks) == 0:
-                self.StartTick = tick 
+                self.StartTick = ticks[0]
             
             # Add tick to list of ticks received
-            self.Ticks = numpy.append(self.Ticks, [tick])
+            self.Ticks = numpy.append(self.Ticks, ticks)
             
             # Update start tick for range if range follow ticks received
             if not self.Fixed or tick < self.StartTick + self.CurrentRange:
@@ -357,8 +357,12 @@
             if self.Fixed and \
                self.Ticks[-1] - self.Ticks[0] < self.CurrentRange:
                 self.Force = True
-        
-        DebugViewer.NewDataAvailable(self, tick, *args, **kwargs)
+            
+            self.HasNewData = False
+            self.RefreshView()
+            
+        else:
+            DebugViewer.NewDataAvailable(self, ticks, *args, **kwargs)
     
     def ForceRefresh(self):
         """
--- a/controls/DebugVariablePanel/DebugVariableItem.py	Sat Oct 12 10:10:30 2013 +0900
+++ b/controls/DebugVariablePanel/DebugVariableItem.py	Mon Oct 14 10:31:32 2013 +0200
@@ -232,52 +232,63 @@
         return (self.Parent.IsNumType(self.VariableType) or 
                 self.VariableType in ["STRING", "WSTRING"])
     
-    def NewValue(self, tick, value, forced=False):
+    def NewValues(self, ticks, values, forced=False):
         """
         Function called by debug thread when a new debug value is available
         @param tick: PLC tick when value was captured
         @param value: Value captured
         @param forced: Forced flag, True if value is forced (default: False)
         """
-        DebugDataConsumer.NewValue(self, tick, value, forced, raw=None)
+        DebugDataConsumer.NewValues(self, ticks, values, forced, raw=None)
         
         if self.Data is not None:
-            # String data value is CRC
-            num_value = (binascii.crc32(value) & STRING_CRC_MASK
-                         if self.VariableType in ["STRING", "WSTRING"]
-                         else float(value))
-            
-            # Update variable range values
-            self.MinValue = (min(self.MinValue, num_value)
-                             if self.MinValue is not None
-                             else num_value)
-            self.MaxValue = (max(self.MaxValue, num_value)
-                             if self.MaxValue is not None
-                             else num_value)
-            
+            
+            if self.VariableType in ["STRING", "WSTRING"]:
+                last_raw_data = (self.RawData[-1]
+                                 if len(self.RawData) > 0 else None)
+                last_raw_data_idx = len(self.RawData) - 1
+                
             # Translate forced flag to float for storing in Data table
             forced_value = float(forced)
             
-            # In the case of string variables, we store raw string value and
-            # forced flag in raw data table. Only changes in this two values
-            # are stored. Index to the corresponding raw value is stored in 
-            # data third column
-            if self.VariableType in ["STRING", "WSTRING"]:
-                raw_data = (value, forced_value)
-                if len(self.RawData) == 0 or self.RawData[-1] != raw_data:
-                    extra_value = len(self.RawData)
-                    self.RawData.append(raw_data)
+            data_values = []
+            for tick, value in zip(ticks, values):
+            
+                # String data value is CRC
+                num_value = (binascii.crc32(value) & STRING_CRC_MASK
+                             if self.VariableType in ["STRING", "WSTRING"]
+                             else float(value))
+            
+                # Update variable range values
+                self.MinValue = (min(self.MinValue, num_value)
+                                 if self.MinValue is not None
+                                 else num_value)
+                self.MaxValue = (max(self.MaxValue, num_value)
+                                 if self.MaxValue is not None
+                                 else num_value)
+            
+                # In the case of string variables, we store raw string value and
+                # forced flag in raw data table. Only changes in this two values
+                # are stored. Index to the corresponding raw value is stored in 
+                # data third column
+                if self.VariableType in ["STRING", "WSTRING"]:
+                    raw_data = (value, forced_value)
+                    if len(self.RawData) == 0 or last_raw_data != raw_data:
+                        last_raw_data_idx += 1
+                        last_raw_data = raw_data
+                        self.RawData.append(raw_data)
+                    extra_value = last_raw_data_idx
+                
+                # In other case, data third column is forced flag
                 else:
-                    extra_value = len(self.RawData) - 1
-            
-            # In other case, data third column is forced flag
-            else:
-                extra_value = forced_value
+                    extra_value = forced_value
+            
+                data_values.append(
+                    [float(tick), num_value, extra_value])
             
             # Add New data to stored data table
-            self.Data = numpy.append(self.Data, 
-                    [[float(tick), num_value, extra_value]], axis=0)
-        
+            self.Data = numpy.append(self.Data, data_values, axis=0)
+            
             # Signal to debug variable panel to refresh
             self.Parent.HasNewData = True
         
--- a/editors/DebugViewer.py	Sat Oct 12 10:10:30 2013 +0900
+++ b/editors/DebugViewer.py	Mon Oct 14 10:31:32 2013 +0200
@@ -214,7 +214,7 @@
             # Search for variable informations in project data
             infos = self.DataProducer.GetInstanceInfos(iec_path)
             if infos is not None:
-                return infos["type"]
+                return infos.type
         
         return None
     
@@ -246,7 +246,7 @@
         if self.DataProducer is not None:
             self.DataProducer.ReleaseDebugIECVariable(iec_path)
     
-    def NewDataAvailable(self, tick, *args, **kwargs):
+    def NewDataAvailable(self, ticks, *args, **kwargs):
         """
         Called by DataProducer for each tick captured
         @param tick: PLC tick captured
--- a/editors/Viewer.py	Sat Oct 12 10:10:30 2013 +0900
+++ b/editors/Viewer.py	Mon Oct 14 10:31:32 2013 +0200
@@ -1053,7 +1053,7 @@
         self.ElementRefreshList.append(element)
         self.ElementRefreshList_lock.release()
         
-    def RefreshNewData(self):
+    def NewDataAvailable(self, ticks, *args, **kwargs):
         refresh_rect = None
         self.ElementRefreshList_lock.acquire()
         for element in self.ElementRefreshList:
@@ -1066,8 +1066,6 @@
         
         if refresh_rect is not None:
             self.RefreshRect(self.GetScrolledRect(refresh_rect), False)
-        else:
-            DebugViewer.RefreshNewData(self)
     
     def SubscribeAllDataConsumers(self):
         self.RefreshView()
--- a/graphics/DebugDataConsumer.py	Sat Oct 12 10:10:30 2013 +0900
+++ b/graphics/DebugDataConsumer.py	Mon Oct 14 10:31:32 2013 +0200
@@ -197,7 +197,7 @@
         """
         self.DataType = data_type
     
-    def NewValue(self, tick, value, forced=False, raw="BOOL"):
+    def NewValues(self, ticks, values, forced=False, raw="BOOL"):
         """
         Function called by debug thread when a new debug value is available
         @param tick: PLC tick when value was captured
@@ -205,6 +205,8 @@
         @param forced: Forced flag, True if value is forced (default: False)
         @param raw: Data type of values not translated (default: 'BOOL')
         """
+        tick, value = ticks[-1], values[-1]
+        
         # Translate value to IEC literal
         if self.DataType != raw:
             value = TYPE_TRANSLATOR.get(self.DataType, str)(value)