Optimierung von Performance und Skalierbarkeit

- 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.
This commit is contained in:
Developer01
2026-10-12 10:01:58 +02:00
parent d15d464034
commit 9fbcb97fe4
5 changed files with 124 additions and 64 deletions

View File

@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
' indem Sie "*" wie unten gezeigt eingeben:
' <Assembly: AssemblyVersion("1.0.*")>
<Assembly: AssemblyVersion("2.1.0.0")>
<Assembly: AssemblyFileVersion("2.1.0.0")>
<Assembly: AssemblyVersion("2.2.0.0")>
<Assembly: AssemblyFileVersion("2.2.0.0")>

View File

@@ -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()

View File

@@ -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)
''' <summary>
''' Wird vom JobListener nach erfolgreichem DatatableJob aufgerufen.
''' Setzt den Snapshot atomar – kein Lock nötig, da Array-Referenzzuweisung auf CLR atomar ist.
''' </summary>
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

View File

@@ -13,7 +13,7 @@ Imports System.Runtime.InteropServices
<Assembly: AssemblyCompany("Digital Data")>
<Assembly: AssemblyProduct("EDMIService")>
<Assembly: AssemblyCopyright("2026")>
<Assembly: AssemblyTrademark("2.6.0")>
<Assembly: AssemblyTrademark("2.8.0")>
<Assembly: ComVisible(False)>
@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
' übernehmen, indem Sie "*" eingeben:
' <Assembly: AssemblyVersion("1.0.*")>
<Assembly: AssemblyVersion("2.6.0.0")>
<Assembly: AssemblyFileVersion("2.6.0.0")>
<Assembly: AssemblyVersion("2.8.0.0")>
<Assembly: AssemblyFileVersion("2.8.0.0")>

View File

@@ -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