using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using Aitex.Core.RT.DataCenter; using Aitex.Core.RT.DBCore; using Aitex.Core.RT.IOCore; using Aitex.Core.RT.Log; using Aitex.Core.RT.SCCore; using Aitex.Core.Util; using MECF.Framework.Common.Equipment; using ThreadState = System.Threading.ThreadState; namespace Aitex.Core.RT.DataCollection.HighPerformance { public class DataTraceManager : Singleton { #region Variables private readonly object _syncRoot = new(); private readonly int _dataRecorderInterval; private readonly int _dataRecorderCacheSize; private readonly int _dataRecorderCachePeriodMs; private string[] _dataTableCategory = { ModuleName.System.ToString() }; private readonly Dictionary> _dataGetters = new(); private readonly R_TRIG _rTrigPersistThreadError = new(); private readonly CancellationTokenSource _ctsDataRecorderThread; private readonly CancellationToken _ctDataRecorderThread; private Dictionary _dictDataRecCachePerDataTable; private DateTime _datePrefixTableName; private readonly Thread _threadCache; private readonly Thread _threadPersist; private bool isAllowTraceAOHopping = false; private bool isAllowTraceDOHopping = false; #endregion #region Constructors public DataTraceManager() { _dataRecorderInterval = (SC.ContainsItem("System.DataRecorderInterval") ? SC.GetValue("System.DataCollectionInterval") : 1000); _dataRecorderCacheSize = (SC.ContainsItem("System.DataRecorderCacheSize") ? SC.GetValue("System.DataRecorderCacheSize") : DataTraceCache.MIN_CACHE_SIZE); _dataRecorderCachePeriodMs = (SC.ContainsItem("System.DataRecorderCachePeriod") ? SC.GetValue("System.DataRecorderCachePeriod") : DataTraceCache.MIN_CACHE_PERIOD_MS); _ctsDataRecorderThread = new(); _ctDataRecorderThread = _ctsDataRecorderThread.Token; _threadCache = new Thread(CacheThread); _threadCache.Name = nameof(CacheThread); _threadPersist = new Thread(PersistThread); _threadPersist.Name = nameof(PersistThread); } #endregion #region Methods /// /// 初始化对象。 /// /// public void Initialize(string[] dataTableCategory) { DATA.Subscribe($"{ModuleName.System}.{ModuleName.Diagnosis}.DataTrace.PersistCostMs", () => _statPersistCostMs); DATA.Subscribe($"{ModuleName.System}.{ModuleName.Diagnosis}.DataTrace.PersistInvMs", () => _statPersistInvMs); DATA.Subscribe($"{ModuleName.System}.{ModuleName.Diagnosis}.DataTrace.PersistLines", () => _statPersistLines); DATA.Subscribe($"{ModuleName.System}.{ModuleName.Diagnosis}.DataTrace.PersistCounter", () => _statPersistCnt); DATA.Subscribe($"{ModuleName.System}.{ModuleName.Diagnosis}.DataTrace.PersistErrorCounter", () => _statPersistErrorCnt); DATA.Subscribe($"{ModuleName.System}.{ModuleName.Diagnosis}.DataTrace.CacheCostMs", () => _statCacheCostMs); DATA.Subscribe($"{ModuleName.System}.{ModuleName.Diagnosis}.DataTrace.CacheInvMs", () => _statCacheInvMs); DATA.Subscribe($"{ModuleName.System}.{ModuleName.Diagnosis}.DataTrace.CacheCounter", () => _statCacheCnt); #if CHECK_DATA_TRACE_OVERRUN_ISSUE DATA.Subscribe($"{ModuleName.System}.{ModuleName.Diagnosis}.DataTrace.CacheOverrunDuration", () => _statCacheOverrunDuration); #endif const string SC_ALLOW_TRACE_AO_HOPPING = "System.AllowTraceAOHopping"; const string SC_ALLOW_TRACE_DO_HOPPING = "System.AllowTraceDOHopping"; isAllowTraceAOHopping = SC.GetValue(SC_ALLOW_TRACE_AO_HOPPING, true); SC.RegisterValueChangedCallback(SC_ALLOW_TRACE_AO_HOPPING, o => { if(!bool.TryParse(o.ToString(), out isAllowTraceAOHopping)) LOG.Error($"Unable to convert value {o} of [{SC_ALLOW_TRACE_AO_HOPPING}] to boolean."); }); isAllowTraceDOHopping = SC.GetValue(SC_ALLOW_TRACE_DO_HOPPING, true); SC.RegisterValueChangedCallback(SC_ALLOW_TRACE_DO_HOPPING, o => { if(!bool.TryParse(o.ToString(), out isAllowTraceDOHopping)) LOG.Error($"Unable to convert value {o} of [{SC_ALLOW_TRACE_DO_HOPPING}] to boolean."); }); _dataTableCategory = dataTableCategory; _threadCache.Start(); } /// /// 立即缓存所有模组的数据。 /// public void ImmediateCache(IIOAccessor io = null, CacheDiagnosisInfo diagnosisInfo = null) { if (io is AOAccessor) { if (!isAllowTraceAOHopping) return; // maybe DB free disk space too low to trace AO hopping var obj = DATA.Poll($"{ModuleName.System}.{ModuleName.Diagnosis}.DiskMon.IsDBFreeDiskSpaceLow"); if (obj is true) return; } if (io is DOAccessor && !isAllowTraceDOHopping) return; if (diagnosisInfo != null) Debug.WriteLine($"{diagnosisInfo.Module}.{diagnosisInfo.IoName} changed to {diagnosisInfo.Value}", $"{nameof(DataTraceManager)} - {nameof(ImmediateCache)}"); lock (_syncRoot) { _tmrCacheThreadDelay.Stop(); } DoCache(); } #region Background Threads private readonly DeviceTimer _tmrPersistDelay = new(); private void PersistThread() { try { while (true) { _ctDataRecorderThread.ThrowIfCancellationRequested(); Thread.Sleep(10); if (!_tmrPersistDelay.IsIdle() && !_tmrPersistDelay.IsTimeout()) continue; // next persist will be called 5sec later _tmrPersistDelay.Start(5000); var ret = Persist(); // check whether the DB write operation is succeeded _rTrigPersistThreadError.CLK = ret < 0; if (_rTrigPersistThreadError.Q) // 插入数据错误,退出 { // DB Write Error LOG.Error( $"{nameof(DataTraceManager)}.{nameof(PersistThread)} Unable to persist the cache."); } } } catch (OperationCanceledException) { LOG.Info($"{nameof(DataTraceManager)}.{nameof(PersistThread)} terminated."); } catch (Exception ex) { LOG.Write(ex, $"{nameof(DataTraceManager)}.{nameof(PersistThread)} terminated unexpectedly,{ex.Message}"); } finally { try { Persist(); } catch (Exception ex) { LOG.Write(ex, $"{nameof(DataTraceManager)}.{nameof(PersistThread)} Failed to persist cache when exit the thread, {ex.Message}"); } } } private readonly DeviceTimer _tmrCacheThreadDelay = new(); private void CacheThread() { try { while (true) { Thread.Sleep(_dataRecorderInterval); _ctDataRecorderThread.ThrowIfCancellationRequested(); CreateCache(); _ctDataRecorderThread.ThrowIfCancellationRequested(); // start persist thread once the cache instances are built. // note the persist thread must be started before CreateCache() method. if (_threadPersist.ThreadState == ThreadState.Unstarted) _threadPersist.Start(); // 开始周期性数据采集 while (true) { _ctDataRecorderThread.ThrowIfCancellationRequested(); Thread.Sleep(10); // 跨天,退出循环并创建新表 if (DateTime.Now.Date != _datePrefixTableName) break; // 立即缓存一次数据 DoCache(); } } } catch (OperationCanceledException) { LOG.Info($"{nameof(DataTraceManager)}.{nameof(CacheThread)} terminated."); } catch (Exception ex) { LOG.Write(ex, $"{nameof(DataTraceManager)}.{nameof(CacheThread)}线程意外终止,{ex.Message}"); } } #endregion /// /// 扫描,获取所有可记录的数据。 /// /// /// 仅数据管理器中注册的数据,且类型为的数据支持保存到数据库。 /// > private void GetRecordableDataSource() { var dBRecorderList = DATA.GetDBRecorderList(); foreach (var item in dBRecorderList) { if (_dataGetters.ContainsKey(item.Key)) continue; var getter = item.Value; var value = getter.Invoke(); if (value != null) { var type = value.GetType(); if(DataTraceHelper.SupportedValueTypes.Contains(type)) _dataGetters[item.Key] = getter; } } } /// /// 创建数据缓存对象。 /// private void CreateCache() { lock (_syncRoot) { // persist the existing data since the date is changing. if (_dictDataRecCachePerDataTable != null) Persist(); GetRecordableDataSource(); _dictDataRecCachePerDataTable = new(); Dictionary> dictDataBufferPerTable = new(); var defaultModuleName = (_dataTableCategory.Contains(ModuleName.System.ToString()) ? ModuleName.System.ToString() : (_dataTableCategory.Contains(ModuleName.System.ToString()) ? ModuleName.System.ToString() : _dataTableCategory[0])); if (_dataGetters is { Count: > 0 }) { foreach (var dataName in _dataGetters.Keys) { Type valueType; Func getter; try { // determine the type of the data. getter = _dataGetters[dataName]; var value = getter.Invoke(); valueType = value.GetType(); Debug.Assert(valueType != null, $"Unable to determine the type of DATA item {dataName}"); } catch (Exception ex) { LOG.Write(ex, $"Unable to determine the type of DATA item {dataName}, {ex.Message}"); Debug.Assert(false, $"Unable to determine the type of DATA item {dataName}"); continue; } var isTableAssigned = false; var categoryName = ""; foreach (var category in _dataTableCategory) { if (dataName.StartsWith(category + ".") || dataName.StartsWith("IO." + category + ".")) { if (!dictDataBufferPerTable.ContainsKey(category)) dictDataBufferPerTable[category] = []; categoryName = category; isTableAssigned = true; break; } } if (!isTableAssigned) { if (!dictDataBufferPerTable.ContainsKey(defaultModuleName)) dictDataBufferPerTable[defaultModuleName] = []; categoryName = defaultModuleName; } // create DataHolder and add to dictDataHolderPerDataTable. var index = dictDataBufferPerTable[categoryName].Count; var type = typeof(DataBuffer<>).MakeGenericType(valueType); var holder = (IDataBuffer)Activator.CreateInstance(type, index, dataName, getter); if (holder == null) { LOG.Error($"Unable to create instance of DataBuffer for {dataName}."); Debug.Assert(holder != null, "Unable to create instance of DataBuffer."); } else { dictDataBufferPerTable[categoryName].Add(holder); } } } _datePrefixTableName = DateTime.Now.Date; foreach (var perTable in dictDataBufferPerTable.Keys) { var holders = dictDataBufferPerTable[perTable]; var tableName = $"{_datePrefixTableName:yyyyMMdd}.{perTable}"; _dictDataRecCachePerDataTable[perTable] = new DataTraceCache(tableName, perTable, holders, minCachePeriodMs: _dataRecorderCachePeriodMs, maxCacheSize: _dataRecorderCacheSize); UpdateTableSchema(tableName, holders); } } } private int _statCacheCostMs; private int _statCacheInvMs; private int _statCacheCnt; private int _statCacheOverrunDuration; private readonly R_TRIG _rTrigTraceOverrun = new(); private readonly Stopwatch _swTraceOverrun = new(); private readonly Stopwatch _swCacheCostMsStat = new(); private readonly Stopwatch _swCacheInvMsStat = new(); /// /// 立即缓存所有模组的数据。 /// /// private void DoCache() { lock (_syncRoot) { if (!_tmrCacheThreadDelay.IsIdle() && !_tmrCacheThreadDelay.IsTimeout()) return; _tmrCacheThreadDelay.Start(_dataRecorderInterval); // statistic cache called interval in ms. _swCacheInvMsStat.Stop(); _statCacheInvMs = (int)_swCacheInvMsStat.Elapsed.TotalMilliseconds; #if CHECK_DATA_TRACE_OVERRUN_ISSUE const int OVERRUN_ALARM_THRESHOLD_M = 60; // Check data-trace-overrun issue _rTrigTraceOverrun.CLK = _statCacheInvMs < 900; if (_rTrigTraceOverrun.Q && !_swTraceOverrun.IsRunning) { _swTraceOverrun.Restart(); } else if (!_rTrigTraceOverrun.M && _swTraceOverrun.IsRunning) { _swTraceOverrun.Stop(); } if (_swTraceOverrun.Elapsed.TotalSeconds > TimeSpan.FromMinutes(OVERRUN_ALARM_THRESHOLD_M).TotalSeconds) { _swTraceOverrun.Stop(); _rTrigTraceOverrun.RST = true; Debug.Assert(false, "Data-Trace-Overrun Issue Detected", $"The {nameof(DataTraceManager)} continues to trace data at a high frequency " + $"within {OVERRUN_ALARM_THRESHOLD_M} minutes. Please check whether there are any Monitor() " + $"methods running frequently to assign values to DO and AO."); } _statCacheOverrunDuration = (int)_swTraceOverrun.Elapsed.TotalSeconds; #endif _swCacheCostMsStat.Restart(); // DO调用ForceCache方法可能发生在当前对象初始化之前,此时_dictCachePerModule为null if (_dictDataRecCachePerDataTable == null) return; // 立即缓存一次数据 foreach (var drc in _dictDataRecCachePerDataTable.Values) drc.Cache(); _swCacheCostMsStat.Stop(); _statCacheCostMs = (int)_swCacheCostMsStat.Elapsed.TotalMilliseconds; _swCacheInvMsStat.Restart(); _statCacheCnt++; } } private readonly Stopwatch _swPersisCostMsStat = new(); private readonly Stopwatch _swPersisInvMsStat = new(); private int _statPersistCostMs; private int _statPersistInvMs; private int _statPersistCnt; private int _statPersistErrorCnt; private int _statPersistLines; /// /// 缓存写入数据库。 /// /// 插入数据库的行数。-1 表示写入数据库错误。 public int Persist() { var effectedLines = 0; lock (_syncRoot) { // statistic persist called interval in ms. _swPersisInvMsStat.Stop(); _statPersistInvMs = (int)_swPersisInvMsStat.Elapsed.TotalMilliseconds; // persist data every 5sec _swPersisCostMsStat.Restart(); foreach (var dcc in _dictDataRecCachePerDataTable.Values) { var sql = dcc.GetInsertSql(); if (!string.IsNullOrEmpty(sql)) { try { var num = DB.ExecuteNonQuery(sql); effectedLines += num; } catch (Exception ex) { LOG.Write(ex, $"{ModuleName.System.ToString()}写入数据库发生异常"); effectedLines = int.MinValue; } } } _swPersisCostMsStat.Stop(); _statPersistCostMs = (int)_swPersisCostMsStat.Elapsed.TotalMilliseconds; } if (effectedLines < 0) { _statPersistErrorCnt++; _statPersistLines = -1; } else { _statPersistCnt++; _statPersistLines = effectedLines; } // start to calculate the interval of persist operation _swPersisInvMsStat.Restart(); return effectedLines; } public void Terminate() { _ctsDataRecorderThread.Cancel(); } private bool UpdateTableSchema(string tblName, IEnumerable holders) { var cmdText = $"select column_name from information_schema.columns where table_name = '{tblName}';"; var reader = DB.ExecuteReader(cmdText); var sqlExprAddColumn = string.Empty; var existedColumns = new List(); while (reader.Read()) { for (var i = 0; i < reader.FieldCount; i++) { existedColumns.Add(reader[i].ToString()); } } reader.Close(); if (existedColumns.Count > 0) { foreach (var holder in holders) { var colName = holder.Name; if (!existedColumns.Contains(colName)) { var type = holder.ValueType; if (type == typeof(bool)) { sqlExprAddColumn += $"ALTER TABLE \"{tblName}\" ADD COLUMN \"{colName}\" {"Boolean"};"; } else if (DataTraceHelper.SupportedValueTypes.Contains(type)) { sqlExprAddColumn += $"ALTER TABLE \"{tblName}\" ADD COLUMN \"{colName}\" {"Real"};"; } } } if (!string.IsNullOrEmpty(sqlExprAddColumn)) { try { DB.ExecuteNonQuery(sqlExprAddColumn); } catch (Exception ex) { LOG.Write(ex); return false; } } } else { var sqlExprCreateTable = $"CREATE TABLE \"{tblName}\"(Time bigint NOT NULL,"; foreach (var dataItem in holders) { var colName = dataItem.Name; var dataType = dataItem.Read().GetType(); if (dataType == typeof(bool)) { sqlExprCreateTable += $"\"{colName}\" Boolean,\n"; } else if (DataTraceHelper.SupportedValueTypes.Contains(dataType)) { sqlExprCreateTable += $"\"{colName}\" Real,\n"; } } sqlExprCreateTable += $"CONSTRAINT \"{tblName}_pkey\" PRIMARY KEY (Time));"; sqlExprCreateTable += $"GRANT SELECT ON TABLE \"{tblName}\" TO postgres;"; try { DB.ExecuteNonQuery(sqlExprCreateTable); } catch (Exception ex2) { LOG.Write(ex2, "创建数据库表格失败"); return false; } } return true; } #endregion } }