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:
|
' indem Sie "*" wie unten gezeigt eingeben:
|
||||||
' <Assembly: AssemblyVersion("1.0.*")>
|
' <Assembly: AssemblyVersion("1.0.*")>
|
||||||
|
|
||||||
<Assembly: AssemblyVersion("2.1.0.0")>
|
<Assembly: AssemblyVersion("2.2.0.0")>
|
||||||
<Assembly: AssemblyFileVersion("2.1.0.0")>
|
<Assembly: AssemblyFileVersion("2.2.0.0")>
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ Public Class ClassSnapPanel
|
|||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Sub ScheduleScrollUpdate()
|
Private Sub ScheduleScrollUpdate()
|
||||||
|
If Not IsHandleCreated Then Return
|
||||||
If Not _NeedsScrollUpdate Then
|
If Not _NeedsScrollUpdate Then
|
||||||
_NeedsScrollUpdate = True
|
_NeedsScrollUpdate = True
|
||||||
BeginInvoke(New MethodInvoker(Sub()
|
BeginInvoke(New MethodInvoker(Sub()
|
||||||
|
|||||||
@@ -71,16 +71,37 @@ Public Class EDMIService
|
|||||||
oThrottling = New ServiceThrottlingBehavior()
|
oThrottling = New ServiceThrottlingBehavior()
|
||||||
Config.Description.Behaviors.Add(oThrottling)
|
Config.Description.Behaviors.Add(oThrottling)
|
||||||
End If
|
End If
|
||||||
|
|
||||||
Dim oCpuCores As Integer = Environment.ProcessorCount
|
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
|
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()
|
_ConfiguredEndpoint = oBaseAddress.ToString()
|
||||||
_ConfiguredMaxCalls = oThrottling.MaxConcurrentCalls
|
_ConfiguredMaxCalls = oThrottling.MaxConcurrentCalls
|
||||||
_ConfiguredMaxSessions = oThrottling.MaxConcurrentSessions
|
_ConfiguredMaxSessions = oThrottling.MaxConcurrentSessions
|
||||||
' ─────────────────────────────────────────────────────────────────────
|
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Public Sub New()
|
Public Sub New()
|
||||||
@@ -201,27 +222,39 @@ Public Class EDMIService
|
|||||||
|
|
||||||
|
|
||||||
#Region "=== Database ==="
|
#Region "=== Database ==="
|
||||||
Private Shared ReadOnly _CachedTablesLock As New Object()
|
' ── CachedTables-Snapshot: wird vom JobListener nach dem DatatableJob gesetzt.
|
||||||
Private Shared _CachedTablesSnapshot As List(Of String) = Nothing
|
' GetCachedTables() liest nur diesen Snapshot – ruft GlobalState nie direkt auf
|
||||||
Private Shared _CachedTablesExpiry As DateTime = DateTime.MinValue
|
' 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
|
Public Function GetCachedTables() As List(Of String) Implements IEDMIService.GetCachedTables
|
||||||
Try
|
Dim oState = _CachedTablesState ' atomarer Read – eine Referenz
|
||||||
SyncLock _CachedTablesLock
|
_Logger.Debug("GetCachedTables: returning snapshot | Count=[{0}] | SnapshotAge=[{1}s]",
|
||||||
If _CachedTablesSnapshot Is Nothing OrElse DateTime.Now > _CachedTablesExpiry Then
|
oState.Tables.Length,
|
||||||
_Logger.Debug("GetCachedTables: Refreshing cache...")
|
CInt((DateTime.Now - oState.Timestamp).TotalSeconds))
|
||||||
_CachedTablesSnapshot = GlobalState.GetCachedTables()
|
Return oState.Tables.ToList()
|
||||||
_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
|
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function ReturnDatatableFromCache(Name As String, FilterExpression As String, SortByColumn As String) As TableResult Implements IEDMIService.ReturnDatatableFromCache
|
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: AssemblyCompany("Digital Data")>
|
||||||
<Assembly: AssemblyProduct("EDMIService")>
|
<Assembly: AssemblyProduct("EDMIService")>
|
||||||
<Assembly: AssemblyCopyright("2026")>
|
<Assembly: AssemblyCopyright("2026")>
|
||||||
<Assembly: AssemblyTrademark("2.6.0")>
|
<Assembly: AssemblyTrademark("2.8.0")>
|
||||||
|
|
||||||
<Assembly: ComVisible(False)>
|
<Assembly: ComVisible(False)>
|
||||||
|
|
||||||
@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
|
|||||||
' übernehmen, indem Sie "*" eingeben:
|
' übernehmen, indem Sie "*" eingeben:
|
||||||
' <Assembly: AssemblyVersion("1.0.*")>
|
' <Assembly: AssemblyVersion("1.0.*")>
|
||||||
|
|
||||||
<Assembly: AssemblyVersion("2.6.0.0")>
|
<Assembly: AssemblyVersion("2.8.0.0")>
|
||||||
<Assembly: AssemblyFileVersion("2.6.0.0")>
|
<Assembly: AssemblyFileVersion("2.8.0.0")>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ Public Class JobListener
|
|||||||
|
|
||||||
' ✅ Verwaltung mit ReaderWriterLockSlim statt Shadow-Copy
|
' ✅ Verwaltung mit ReaderWriterLockSlim statt Shadow-Copy
|
||||||
Private ReadOnly _Dataset As DataSet
|
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 _Logger As Logger
|
||||||
Private ReadOnly _LogConfig As LogConfig
|
Private ReadOnly _LogConfig As LogConfig
|
||||||
Private ReadOnly _MSSQL As MSSQLServer
|
Private ReadOnly _MSSQL As MSSQLServer
|
||||||
@@ -61,7 +61,13 @@ Public Class JobListener
|
|||||||
' ✅ Thread-sicherer Zugriff auf Dataset (nur für GetCachedTables)
|
' ✅ Thread-sicherer Zugriff auf Dataset (nur für GetCachedTables)
|
||||||
Public Function GetCurrentDataset() As DataSet
|
Public Function GetCurrentDataset() As DataSet
|
||||||
' Keine Kopie - nur für Metadaten-Zugriff (Tabellennamen)
|
' Keine Kopie - nur für Metadaten-Zugriff (Tabellennamen)
|
||||||
|
' HINWEIS: Aufrufer darf keine strukturellen Änderungen vornehmen!
|
||||||
|
_DatasetLock.EnterReadLock()
|
||||||
|
Try
|
||||||
Return _Dataset
|
Return _Dataset
|
||||||
|
Finally
|
||||||
|
_DatasetLock.ExitReadLock()
|
||||||
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Overrides Function JobWasExecuted(context As IJobExecutionContext, jobException As JobExecutionException, Optional cancellationToken As CancellationToken = Nothing) As Task
|
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
|
End Try
|
||||||
Next
|
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)
|
Return MyBase.JobWasExecuted(context, jobException, cancellationToken)
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
' ✅ OPTIMIERT: SaveDataTables mit direktem Write-Lock
|
|
||||||
Public Sub SaveDataTables(Result As JobResult.ResultTable)
|
Public Sub SaveDataTables(Result As JobResult.ResultTable)
|
||||||
Dim oStopWatch As New Stopwatch()
|
Dim oStopWatch As New Stopwatch()
|
||||||
|
|
||||||
@@ -94,51 +115,56 @@ Public Class JobListener
|
|||||||
|
|
||||||
_Logger.Debug("Starting thread-safe update for table [{0}]", oName)
|
_Logger.Debug("Starting thread-safe update for table [{0}]", oName)
|
||||||
|
|
||||||
' ✅ SCHRITT 1: Write-Lock anfordern (blockiert Leser während Update)
|
' SCHRITT 1: Neue Tabelle vorbereiten – AUSSERHALB des Locks (teuer!)
|
||||||
oStopWatch.Start()
|
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()
|
_DatasetLock.EnterWriteLock()
|
||||||
_Logger.Debug("[Lock] Write-Lock acquired for table [{0}]", oName)
|
_Logger.Debug("[Lock] Write-Lock acquired for table [{0}]", oName)
|
||||||
|
|
||||||
Try
|
Try
|
||||||
' ✅ SCHRITT 2: Direkte Änderungen im Dataset
|
' Bestehende Tabelle entfernen
|
||||||
If _Dataset.Tables.Contains(oName) Then
|
If _Dataset.Tables.Contains(oName) Then
|
||||||
_Logger.Debug("Datatable [{0}] already exists, replacing.", oName)
|
Dim oExisting = _Dataset.Tables(oName)
|
||||||
|
oExisting.Constraints.Clear()
|
||||||
If Result.ChildTable IsNot Nothing Then
|
oExisting.ChildRelations.Clear()
|
||||||
ReplaceExistingTable(oName, oTable, _Dataset, Result.ChildTable)
|
_Dataset.Tables.Remove(oName)
|
||||||
AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn, _Dataset)
|
|
||||||
Else
|
|
||||||
ReplaceExistingTable(oName, oTable, _Dataset)
|
|
||||||
End If
|
End If
|
||||||
Else
|
_Dataset.Tables.Add(oNewTable)
|
||||||
_Logger.Debug("Datatable [{0}] does not exist yet, adding.", oName)
|
|
||||||
|
|
||||||
AddNewTable(oName, oTable, _Dataset)
|
' Child-Tabelle tauschen
|
||||||
|
If oNewChild IsNot Nothing Then
|
||||||
If Result.ChildTable IsNot Nothing Then
|
If _Dataset.Tables.Contains(oNewChild.TableName) Then
|
||||||
AddNewTable(Result.ChildTable.TableName, Result.ChildTable, _Dataset)
|
_Dataset.Tables(oNewChild.TableName).Constraints.Clear()
|
||||||
AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn, _Dataset)
|
_Dataset.Tables(oNewChild.TableName).ChildRelations.Clear()
|
||||||
|
_Dataset.Tables.Remove(oNewChild.TableName)
|
||||||
End If
|
End If
|
||||||
|
_Dataset.Tables.Add(oNewChild)
|
||||||
|
AddRelation(oName, Result.TableRelationColumn, oNewChild.TableName, Result.ChildRelationColumn, _Dataset)
|
||||||
End If
|
End If
|
||||||
|
|
||||||
_Logger.Info("✔ Dataset successfully updated with table [{0}]", oName)
|
|
||||||
|
|
||||||
Finally
|
Finally
|
||||||
' ✅ SCHRITT 3: Write-Lock freigeben (so schnell wie möglich!)
|
|
||||||
_DatasetLock.ExitWriteLock()
|
_DatasetLock.ExitWriteLock()
|
||||||
oStopWatch.Stop()
|
oStopWatch.Stop()
|
||||||
' ✅ Performance-Warning bei langsamen Updates
|
' ← Warnung wenn Lock > 50ms (war 100ms – zu tolerant)
|
||||||
If oStopWatch.ElapsedMilliseconds > 500 Then
|
If oStopWatch.ElapsedMilliseconds > 50 Then
|
||||||
_Logger.Warn("⚠ [Performance] Table update took {0}ms (> 500ms threshold)", oStopWatch.ElapsedMilliseconds)
|
_Logger.Warn("⚠ [Performance] Write-Lock held for {0}ms (> 50ms) on table [{1}] | Rows=[{2}]",
|
||||||
|
oStopWatch.ElapsedMilliseconds, oName, oNewTable.Rows.Count)
|
||||||
Else
|
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 If
|
||||||
End Try
|
End Try
|
||||||
|
|
||||||
' ✅ SCHRITT 4: Speichern in Historie (außerhalb des Locks)
|
' SCHRITT 3: Logging + Historie – außerhalb des Locks
|
||||||
Dim oMemoryUsage = GC.GetTotalMemory(True)
|
_Logger.Info("✔ Dataset updated | Table=[{0}] | Rows=[{1}]", oName, oNewTable.Rows.Count)
|
||||||
_Logger.Debug("Estimated memory usage after Job is [{0}]", FormatBytes(oMemoryUsage))
|
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})")
|
_MSSQL.ExecuteNonQuery($"INSERT INTO TBAPPSERV_CRON_DETAIL_HISTORY (DETAIL_ID) VALUES ({oDetailId})")
|
||||||
|
|
||||||
Catch ex As Exception
|
Catch ex As Exception
|
||||||
|
|||||||
Reference in New Issue
Block a user