From 9fbcb97fe4d93ce227bf826768650e88e9b737ce Mon Sep 17 00:00:00 2001 From: Developer01 Date: Mon, 12 Oct 2026 10:01:58 +0200 Subject: [PATCH] Optimierung von Performance und Skalierbarkeit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Assembly-Versionen auf 2.2.0.0 und 2.8.0.0 aktualisiert. - Verbesserte Steuerung von `ScheduleScrollUpdate` in SnapPanel. - Erhöhte Serverkapazität für 300+ Benutzer in EDMIService. - Einführung von Windows-Authentifizierungs-Caching. - Neuer atomarer Tabellen-Caching-Mechanismus in EDMIService. - Lock-Mechanismus in JobListener optimiert (Instanz-Lock). - Verbesserte Performance durch kürzere Lock-Dauern in SaveDataTables. - Logging und Snapshot-Veröffentlichung nach Job-Ausführung optimiert. - Allgemeine Skalierungs- und Performance-Verbesserungen. --- Controls.SnapPanel/My Project/AssemblyInfo.vb | 4 +- Controls.SnapPanel/SnapPanel.vb | 1 + Service.EDMIService/EDMIService.vb | 79 ++++++++++----- .../My Project/AssemblyInfo.vb | 6 +- Service.EDMIService/Scheduler/JobListener.vb | 98 ++++++++++++------- 5 files changed, 124 insertions(+), 64 deletions(-) diff --git a/Controls.SnapPanel/My Project/AssemblyInfo.vb b/Controls.SnapPanel/My Project/AssemblyInfo.vb index 43d36dc4..650bbd2b 100644 --- a/Controls.SnapPanel/My Project/AssemblyInfo.vb +++ b/Controls.SnapPanel/My Project/AssemblyInfo.vb @@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices ' indem Sie "*" wie unten gezeigt eingeben: ' - - + + diff --git a/Controls.SnapPanel/SnapPanel.vb b/Controls.SnapPanel/SnapPanel.vb index ec2cac17..5659b8d6 100644 --- a/Controls.SnapPanel/SnapPanel.vb +++ b/Controls.SnapPanel/SnapPanel.vb @@ -78,6 +78,7 @@ Public Class ClassSnapPanel End Sub Private Sub ScheduleScrollUpdate() + If Not IsHandleCreated Then Return If Not _NeedsScrollUpdate Then _NeedsScrollUpdate = True BeginInvoke(New MethodInvoker(Sub() diff --git a/Service.EDMIService/EDMIService.vb b/Service.EDMIService/EDMIService.vb index 5a7da31e..54512f66 100644 --- a/Service.EDMIService/EDMIService.vb +++ b/Service.EDMIService/EDMIService.vb @@ -71,16 +71,37 @@ Public Class EDMIService oThrottling = New ServiceThrottlingBehavior() Config.Description.Behaviors.Add(oThrottling) End If + Dim oCpuCores As Integer = Environment.ProcessorCount - oThrottling.MaxConcurrentCalls = Math.Max(200, 32 * oCpuCores) ' war 16× - oThrottling.MaxConcurrentSessions = Math.Max(400, 150 * oCpuCores) ' war 100× + + ' ← Server-seitige Kapazität für 300+ User + oThrottling.MaxConcurrentCalls = Math.Max(400, 32 * oCpuCores) + oThrottling.MaxConcurrentSessions = Math.Max(600, 150 * oCpuCores) oThrottling.MaxConcurrentInstances = Integer.MaxValue - ' ── Werte für späteres Logging speichern ───────────────────────────── + ' ← Server-seitiges Binding: toleriert langsamere Clients (alte DLL!) + Dim oNetTcpBinding = TryCast(oBinding, NetTcpBinding) + If oNetTcpBinding IsNot Nothing Then + ' ← Konsistent mit Channel.GetBinding(): CPUs × 50, mind. 500 + Dim oMaxConnections = Math.Max(500, oCpuCores * 50) + oNetTcpBinding.SendTimeout = TimeSpan.FromMinutes(5) + oNetTcpBinding.ReceiveTimeout = TimeSpan.FromMinutes(10) + oNetTcpBinding.OpenTimeout = TimeSpan.FromMinutes(2) + oNetTcpBinding.MaxConnections = oMaxConnections + LogConfig?.GetLogger()?.Debug("✔ NetTcpBinding configured | MaxConnections=[{0}] | SendTimeout=[{1}]", + oMaxConnections, oNetTcpBinding.SendTimeout) + End If + ' ← NEU: Windows Auth Caching – reduziert DC-Anfragen bei parallelen Verbindungen + Dim oServiceCredentials = Config.Description.Behaviors.Find(Of ServiceCredentials)() + If oServiceCredentials Is Nothing Then + oServiceCredentials = New ServiceCredentials() + Config.Description.Behaviors.Add(oServiceCredentials) + End If + ' Gruppenauflösung deaktivieren: weniger DC-Roundtrips pro Verbindung + oServiceCredentials.WindowsAuthentication.IncludeWindowsGroups = False _ConfiguredEndpoint = oBaseAddress.ToString() _ConfiguredMaxCalls = oThrottling.MaxConcurrentCalls _ConfiguredMaxSessions = oThrottling.MaxConcurrentSessions - ' ───────────────────────────────────────────────────────────────────── End Sub Public Sub New() @@ -201,27 +222,39 @@ Public Class EDMIService #Region "=== Database ===" - Private Shared ReadOnly _CachedTablesLock As New Object() - Private Shared _CachedTablesSnapshot As List(Of String) = Nothing - Private Shared _CachedTablesExpiry As DateTime = DateTime.MinValue + ' ── CachedTables-Snapshot: wird vom JobListener nach dem DatatableJob gesetzt. + ' GetCachedTables() liest nur diesen Snapshot – ruft GlobalState nie direkt auf + ' und blockiert daher nicht, auch wenn der DatatableJob den TableStore sperrt. + Private Class CachedTablesState + Public ReadOnly Tables As String() + Public ReadOnly Timestamp As DateTime + Public Sub New(pTables As String(), pTimestamp As DateTime) + Tables = pTables + Timestamp = pTimestamp + End Sub + End Class + + Private Shared _CachedTablesState As CachedTablesState = New CachedTablesState(Array.Empty(Of String)(), DateTime.MinValue) + + ''' + ''' Wird vom JobListener nach erfolgreichem DatatableJob aufgerufen. + ''' Setzt den Snapshot atomar – kein Lock nötig, da Array-Referenzzuweisung auf CLR atomar ist. + ''' + Public Shared Sub PublishCachedTablesSnapshot(pTableNames As IEnumerable(Of String)) + Dim oTables = pTableNames.Select(Function(t) t.ToUpper()).ToArray() + Dim oNewState = New CachedTablesState(oTables, DateTime.Now) + ' Atomar: Timestamp und Array immer gemeinsam sichtbar + Interlocked.Exchange(_CachedTablesState, oNewState) + LogConfig?.GetLogger()?.Debug("PublishCachedTablesSnapshot: [{0}] tables published at [{1}]", + oTables.Length, oNewState.Timestamp.ToString("HH:mm:ss")) + End Sub Public Function GetCachedTables() As List(Of String) Implements IEDMIService.GetCachedTables - Try - SyncLock _CachedTablesLock - If _CachedTablesSnapshot Is Nothing OrElse DateTime.Now > _CachedTablesExpiry Then - _Logger.Debug("GetCachedTables: Refreshing cache...") - _CachedTablesSnapshot = GlobalState.GetCachedTables() - _CachedTablesExpiry = DateTime.Now.AddSeconds(30) - _Logger.Debug("GetCachedTables: Cache refreshed | Count=[{0}] | NextExpiry=[{1}]", - _CachedTablesSnapshot.Count, _CachedTablesExpiry.ToString("HH:mm:ss")) - End If - Return _CachedTablesSnapshot.ToList() - End SyncLock - Catch ex As Exception - _Logger.Error("GetCachedTables failed | User=[{0}] | Error=[{1}]", _Username, ex.Message) - _Logger.Error(ex) - Return New List(Of String) - End Try + Dim oState = _CachedTablesState ' atomarer Read – eine Referenz + _Logger.Debug("GetCachedTables: returning snapshot | Count=[{0}] | SnapshotAge=[{1}s]", + oState.Tables.Length, + CInt((DateTime.Now - oState.Timestamp).TotalSeconds)) + Return oState.Tables.ToList() End Function Public Function ReturnDatatableFromCache(Name As String, FilterExpression As String, SortByColumn As String) As TableResult Implements IEDMIService.ReturnDatatableFromCache diff --git a/Service.EDMIService/My Project/AssemblyInfo.vb b/Service.EDMIService/My Project/AssemblyInfo.vb index 8ffdcb7c..9dbea938 100644 --- a/Service.EDMIService/My Project/AssemblyInfo.vb +++ b/Service.EDMIService/My Project/AssemblyInfo.vb @@ -13,7 +13,7 @@ Imports System.Runtime.InteropServices - + @@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices ' übernehmen, indem Sie "*" eingeben: ' - - + + diff --git a/Service.EDMIService/Scheduler/JobListener.vb b/Service.EDMIService/Scheduler/JobListener.vb index 46da23a3..a97c5e42 100644 --- a/Service.EDMIService/Scheduler/JobListener.vb +++ b/Service.EDMIService/Scheduler/JobListener.vb @@ -14,8 +14,8 @@ Public Class JobListener ' ✅ Verwaltung mit ReaderWriterLockSlim statt Shadow-Copy Private ReadOnly _Dataset As DataSet - Private Shared ReadOnly _DatasetLock As New ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion) - + ' Instanz-Lock statt Shared, damit mehrere Instanzen sich nicht gegenseitig blockieren + Private ReadOnly _DatasetLock As New ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion) Private ReadOnly _Logger As Logger Private ReadOnly _LogConfig As LogConfig Private ReadOnly _MSSQL As MSSQLServer @@ -61,7 +61,13 @@ Public Class JobListener ' ✅ Thread-sicherer Zugriff auf Dataset (nur für GetCachedTables) Public Function GetCurrentDataset() As DataSet ' Keine Kopie - nur für Metadaten-Zugriff (Tabellennamen) - Return _Dataset + ' HINWEIS: Aufrufer darf keine strukturellen Änderungen vornehmen! + _DatasetLock.EnterReadLock() + Try + Return _Dataset + Finally + _DatasetLock.ExitReadLock() + End Try End Function Public Overrides Function JobWasExecuted(context As IJobExecutionContext, jobException As JobExecutionException, Optional cancellationToken As CancellationToken = Nothing) As Task @@ -80,10 +86,25 @@ Public Class JobListener End Try Next + ' ✅ Snapshot NACH allen Tabellen publizieren – direkt aus _Dataset, nicht aus GlobalState. + ' Vermeidet Blockierung durch DatatableJob-Lock auf GlobalState/TableStore. + Try + Dim oTableNames As IEnumerable(Of String) + _DatasetLock.EnterReadLock() + Try + oTableNames = _Dataset.Tables.Cast(Of DataTable)().Select(Function(t) t.TableName).ToList() + Finally + _DatasetLock.ExitReadLock() + End Try + + EDMIService.PublishCachedTablesSnapshot(oTableNames) + _Logger.Info("✔ CachedTables snapshot published | Count=[{0}]", oTableNames.Count) + Catch ex As Exception + _Logger.Warn("⚠ PublishCachedTablesSnapshot failed: {0}", ex.Message) + End Try Return MyBase.JobWasExecuted(context, jobException, cancellationToken) End Function - ' ✅ OPTIMIERT: SaveDataTables mit direktem Write-Lock Public Sub SaveDataTables(Result As JobResult.ResultTable) Dim oStopWatch As New Stopwatch() @@ -94,51 +115,56 @@ Public Class JobListener _Logger.Debug("Starting thread-safe update for table [{0}]", oName) - ' ✅ SCHRITT 1: Write-Lock anfordern (blockiert Leser während Update) - oStopWatch.Start() + ' SCHRITT 1: Neue Tabelle vorbereiten – AUSSERHALB des Locks (teuer!) + Dim oNewTable As DataTable = oTable.Copy() + oNewTable.TableName = oName + + Dim oNewChild As DataTable = Nothing + If Result.ChildTable IsNot Nothing Then + oNewChild = Result.ChildTable.Copy() + oNewChild.TableName = Result.ChildTable.TableName + End If + + ' SCHRITT 2: Write-Lock nur für den atomaren Swap + oStopWatch.Restart() _DatasetLock.EnterWriteLock() _Logger.Debug("[Lock] Write-Lock acquired for table [{0}]", oName) - Try - ' ✅ SCHRITT 2: Direkte Änderungen im Dataset + ' Bestehende Tabelle entfernen If _Dataset.Tables.Contains(oName) Then - _Logger.Debug("Datatable [{0}] already exists, replacing.", oName) - - If Result.ChildTable IsNot Nothing Then - ReplaceExistingTable(oName, oTable, _Dataset, Result.ChildTable) - AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn, _Dataset) - Else - ReplaceExistingTable(oName, oTable, _Dataset) - End If - Else - _Logger.Debug("Datatable [{0}] does not exist yet, adding.", oName) - - AddNewTable(oName, oTable, _Dataset) - - If Result.ChildTable IsNot Nothing Then - AddNewTable(Result.ChildTable.TableName, Result.ChildTable, _Dataset) - AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn, _Dataset) - End If + Dim oExisting = _Dataset.Tables(oName) + oExisting.Constraints.Clear() + oExisting.ChildRelations.Clear() + _Dataset.Tables.Remove(oName) End If + _Dataset.Tables.Add(oNewTable) - _Logger.Info("✔ Dataset successfully updated with table [{0}]", oName) - + ' Child-Tabelle tauschen + If oNewChild IsNot Nothing Then + If _Dataset.Tables.Contains(oNewChild.TableName) Then + _Dataset.Tables(oNewChild.TableName).Constraints.Clear() + _Dataset.Tables(oNewChild.TableName).ChildRelations.Clear() + _Dataset.Tables.Remove(oNewChild.TableName) + End If + _Dataset.Tables.Add(oNewChild) + AddRelation(oName, Result.TableRelationColumn, oNewChild.TableName, Result.ChildRelationColumn, _Dataset) + End If Finally - ' ✅ SCHRITT 3: Write-Lock freigeben (so schnell wie möglich!) _DatasetLock.ExitWriteLock() oStopWatch.Stop() - ' ✅ Performance-Warning bei langsamen Updates - If oStopWatch.ElapsedMilliseconds > 500 Then - _Logger.Warn("⚠ [Performance] Table update took {0}ms (> 500ms threshold)", oStopWatch.ElapsedMilliseconds) + ' ← Warnung wenn Lock > 50ms (war 100ms – zu tolerant) + If oStopWatch.ElapsedMilliseconds > 50 Then + _Logger.Warn("⚠ [Performance] Write-Lock held for {0}ms (> 50ms) on table [{1}] | Rows=[{2}]", + oStopWatch.ElapsedMilliseconds, oName, oNewTable.Rows.Count) Else - _Logger.Debug("[Performance] Table update completed in {0}ms", oStopWatch.ElapsedMilliseconds) + _Logger.Debug("[Performance] Table update in {0}ms for [{1}]", oStopWatch.ElapsedMilliseconds, oName) End If End Try - ' ✅ SCHRITT 4: Speichern in Historie (außerhalb des Locks) - Dim oMemoryUsage = GC.GetTotalMemory(True) - _Logger.Debug("Estimated memory usage after Job is [{0}]", FormatBytes(oMemoryUsage)) - + ' SCHRITT 3: Logging + Historie – außerhalb des Locks + _Logger.Info("✔ Dataset updated | Table=[{0}] | Rows=[{1}]", oName, oNewTable.Rows.Count) + Dim oMemoryUsage = GC.GetTotalMemory(False) + _Logger.Debug("Memory after Job: [{0}]", FormatBytes(oMemoryUsage)) _MSSQL.ExecuteNonQuery($"INSERT INTO TBAPPSERV_CRON_DETAIL_HISTORY (DETAIL_ID) VALUES ({oDetailId})") Catch ex As Exception