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:
@@ -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")>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")>
|
||||
|
||||
@@ -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)
|
||||
' 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)
|
||||
Dim oExisting = _Dataset.Tables(oName)
|
||||
oExisting.Constraints.Clear()
|
||||
oExisting.ChildRelations.Clear()
|
||||
_Dataset.Tables.Remove(oName)
|
||||
End If
|
||||
Else
|
||||
_Logger.Debug("Datatable [{0}] does not exist yet, adding.", oName)
|
||||
_Dataset.Tables.Add(oNewTable)
|
||||
|
||||
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)
|
||||
' 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
|
||||
|
||||
_Logger.Info("✔ Dataset successfully updated with table [{0}]", oName)
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user