@@ -24,9 +24,9 @@ namespace PerformanceMonitorLite.Services;
2424public class DeltaCalculator
2525{
2626 /// <summary>
27- /// Cache structure: serverId -> collectorName -> key -> previousValue
27+ /// Cache structure: serverId -> collectorName -> key -> ( previousValue, timestamp)
2828 /// </summary>
29- private readonly ConcurrentDictionary < int , ConcurrentDictionary < string , ConcurrentDictionary < string , long > > > _cache = new ( ) ;
29+ private readonly ConcurrentDictionary < int , ConcurrentDictionary < string , ConcurrentDictionary < string , ( long Value , DateTime ? Timestamp ) > > > _cache = new ( ) ;
3030
3131 private readonly ILogger ? _logger ;
3232
@@ -63,12 +63,15 @@ public async Task SeedFromDatabaseAsync(DuckDbInitializer duckDb)
6363 /// Calculates the delta between the current value and the previous cached value.
6464 /// First-ever sighting (no baseline): returns currentValue so single-execution queries appear.
6565 /// Counter reset (value decreased): returns 0 to avoid inflated deltas from plan cache churn.
66+ /// Gap detection: if collectionTime and maxGapSeconds are provided and the gap since the
67+ /// last cached value exceeds maxGapSeconds, returns 0 to avoid inflated deltas after restarts.
6668 /// Thread-safe via atomic AddOrUpdate.
6769 /// </summary>
68- public long CalculateDelta ( int serverId , string collectorName , string key , long currentValue , bool baselineOnly = false )
70+ public long CalculateDelta ( int serverId , string collectorName , string key , long currentValue ,
71+ bool baselineOnly = false , DateTime ? collectionTime = null , int maxGapSeconds = 0 )
6972 {
70- var serverCache = _cache . GetOrAdd ( serverId , _ => new ConcurrentDictionary < string , ConcurrentDictionary < string , long > > ( ) ) ;
71- var collectorCache = serverCache . GetOrAdd ( collectorName , _ => new ConcurrentDictionary < string , long > ( ) ) ;
73+ var serverCache = _cache . GetOrAdd ( serverId , _ => new ConcurrentDictionary < string , ConcurrentDictionary < string , ( long Value , DateTime ? Timestamp ) > > ( ) ) ;
74+ var collectorCache = serverCache . GetOrAdd ( collectorName , _ => new ConcurrentDictionary < string , ( long Value , DateTime ? Timestamp ) > ( ) ) ;
7275
7376 long delta = 0 ;
7477
@@ -80,15 +83,24 @@ public long CalculateDelta(int serverId, string collectorName, string key, long
8083 _ =>
8184 {
8285 delta = baselineOnly ? 0 : currentValue ;
83- return currentValue ;
86+ return ( currentValue , collectionTime ) ;
8487 } ,
8588 /* Update: compute delta atomically */
86- ( _ , previousValue ) =>
89+ ( _ , previous ) =>
8790 {
88- delta = currentValue < previousValue
91+ /* Gap detection: if too much time has passed since the last cached value,
92+ treat this as a new baseline to avoid inflated deltas after app restarts */
93+ if ( maxGapSeconds > 0 && collectionTime . HasValue && previous . Timestamp . HasValue
94+ && ( collectionTime . Value - previous . Timestamp . Value ) . TotalSeconds > maxGapSeconds )
95+ {
96+ delta = 0 ;
97+ return ( currentValue , collectionTime ) ;
98+ }
99+
100+ delta = currentValue < previous . Value
89101 ? 0 /* counter reset (plan cache eviction/re-entry) — not real new work */
90- : currentValue - previousValue ;
91- return currentValue ;
102+ : currentValue - previous . Value ;
103+ return ( currentValue , collectionTime ) ;
92104 } ) ;
93105
94106 return delta ;
@@ -97,18 +109,18 @@ public long CalculateDelta(int serverId, string collectorName, string key, long
97109 /// <summary>
98110 /// Seeds a single value into the cache without computing a delta.
99111 /// </summary>
100- private void Seed ( int serverId , string collectorName , string key , long value )
112+ private void Seed ( int serverId , string collectorName , string key , long value , DateTime ? timestamp = null )
101113 {
102- var serverCache = _cache . GetOrAdd ( serverId , _ => new ConcurrentDictionary < string , ConcurrentDictionary < string , long > > ( ) ) ;
103- var collectorCache = serverCache . GetOrAdd ( collectorName , _ => new ConcurrentDictionary < string , long > ( ) ) ;
104- collectorCache [ key ] = value ;
114+ var serverCache = _cache . GetOrAdd ( serverId , _ => new ConcurrentDictionary < string , ConcurrentDictionary < string , ( long Value , DateTime ? Timestamp ) > > ( ) ) ;
115+ var collectorCache = serverCache . GetOrAdd ( collectorName , _ => new ConcurrentDictionary < string , ( long Value , DateTime ? Timestamp ) > ( ) ) ;
116+ collectorCache [ key ] = ( value , timestamp ) ;
105117 }
106118
107119 private async Task SeedWaitStatsAsync ( DuckDBConnection connection )
108120 {
109121 using var cmd = connection . CreateCommand ( ) ;
110122 cmd . CommandText = @"
111- SELECT server_id, wait_type, waiting_tasks_count, wait_time_ms, signal_wait_time_ms
123+ SELECT server_id, wait_type, waiting_tasks_count, wait_time_ms, signal_wait_time_ms, collection_time
112124FROM wait_stats
113125WHERE (server_id, collection_time) IN (
114126 SELECT server_id, MAX(collection_time) FROM wait_stats GROUP BY server_id
@@ -119,9 +131,10 @@ FROM wait_stats
119131 {
120132 var serverId = reader . GetInt32 ( 0 ) ;
121133 var waitType = reader . GetString ( 1 ) ;
122- Seed ( serverId , "wait_stats_tasks" , waitType , reader . GetInt64 ( 2 ) ) ;
123- Seed ( serverId , "wait_stats_time" , waitType , reader . GetInt64 ( 3 ) ) ;
124- Seed ( serverId , "wait_stats_signal" , waitType , reader . GetInt64 ( 4 ) ) ;
134+ var ts = reader . IsDBNull ( 5 ) ? ( DateTime ? ) null : reader . GetDateTime ( 5 ) ;
135+ Seed ( serverId , "wait_stats_tasks" , waitType , reader . GetInt64 ( 2 ) , ts ) ;
136+ Seed ( serverId , "wait_stats_time" , waitType , reader . GetInt64 ( 3 ) , ts ) ;
137+ Seed ( serverId , "wait_stats_signal" , waitType , reader . GetInt64 ( 4 ) , ts ) ;
125138 count ++ ;
126139 }
127140 if ( count > 0 ) _logger ? . LogDebug ( "Seeded {Count} wait_stats baseline rows" , count ) ;
@@ -134,7 +147,8 @@ private async Task SeedFileIoStatsAsync(DuckDBConnection connection)
134147SELECT server_id, database_name, file_name,
135148 num_of_reads, num_of_writes, read_bytes, write_bytes,
136149 io_stall_read_ms, io_stall_write_ms,
137- io_stall_queued_read_ms, io_stall_queued_write_ms
150+ io_stall_queued_read_ms, io_stall_queued_write_ms,
151+ collection_time
138152FROM file_io_stats
139153WHERE (server_id, collection_time) IN (
140154 SELECT server_id, MAX(collection_time) FROM file_io_stats GROUP BY server_id
@@ -147,14 +161,15 @@ FROM file_io_stats
147161 var dbName = reader . IsDBNull ( 1 ) ? "" : reader . GetString ( 1 ) ;
148162 var fileName = reader . IsDBNull ( 2 ) ? "" : reader . GetString ( 2 ) ;
149163 var deltaKey = $ "{ dbName } |{ fileName } ";
150- Seed ( serverId , "file_io_reads" , deltaKey , reader . IsDBNull ( 3 ) ? 0 : reader . GetInt64 ( 3 ) ) ;
151- Seed ( serverId , "file_io_writes" , deltaKey , reader . IsDBNull ( 4 ) ? 0 : reader . GetInt64 ( 4 ) ) ;
152- Seed ( serverId , "file_io_read_bytes" , deltaKey , reader . IsDBNull ( 5 ) ? 0 : reader . GetInt64 ( 5 ) ) ;
153- Seed ( serverId , "file_io_write_bytes" , deltaKey , reader . IsDBNull ( 6 ) ? 0 : reader . GetInt64 ( 6 ) ) ;
154- Seed ( serverId , "file_io_stall_read" , deltaKey , reader . IsDBNull ( 7 ) ? 0 : reader . GetInt64 ( 7 ) ) ;
155- Seed ( serverId , "file_io_stall_write" , deltaKey , reader . IsDBNull ( 8 ) ? 0 : reader . GetInt64 ( 8 ) ) ;
156- Seed ( serverId , "file_io_stall_queued_read" , deltaKey , reader . IsDBNull ( 9 ) ? 0 : reader . GetInt64 ( 9 ) ) ;
157- Seed ( serverId , "file_io_stall_queued_write" , deltaKey , reader . IsDBNull ( 10 ) ? 0 : reader . GetInt64 ( 10 ) ) ;
164+ var ts = reader . IsDBNull ( 11 ) ? ( DateTime ? ) null : reader . GetDateTime ( 11 ) ;
165+ Seed ( serverId , "file_io_reads" , deltaKey , reader . IsDBNull ( 3 ) ? 0 : reader . GetInt64 ( 3 ) , ts ) ;
166+ Seed ( serverId , "file_io_writes" , deltaKey , reader . IsDBNull ( 4 ) ? 0 : reader . GetInt64 ( 4 ) , ts ) ;
167+ Seed ( serverId , "file_io_read_bytes" , deltaKey , reader . IsDBNull ( 5 ) ? 0 : reader . GetInt64 ( 5 ) , ts ) ;
168+ Seed ( serverId , "file_io_write_bytes" , deltaKey , reader . IsDBNull ( 6 ) ? 0 : reader . GetInt64 ( 6 ) , ts ) ;
169+ Seed ( serverId , "file_io_stall_read" , deltaKey , reader . IsDBNull ( 7 ) ? 0 : reader . GetInt64 ( 7 ) , ts ) ;
170+ Seed ( serverId , "file_io_stall_write" , deltaKey , reader . IsDBNull ( 8 ) ? 0 : reader . GetInt64 ( 8 ) , ts ) ;
171+ Seed ( serverId , "file_io_stall_queued_read" , deltaKey , reader . IsDBNull ( 9 ) ? 0 : reader . GetInt64 ( 9 ) , ts ) ;
172+ Seed ( serverId , "file_io_stall_queued_write" , deltaKey , reader . IsDBNull ( 10 ) ? 0 : reader . GetInt64 ( 10 ) , ts ) ;
158173 count ++ ;
159174 }
160175 if ( count > 0 ) _logger ? . LogDebug ( "Seeded {Count} file_io_stats baseline rows" , count ) ;
@@ -164,7 +179,7 @@ private async Task SeedPerfmonStatsAsync(DuckDBConnection connection)
164179 {
165180 using var cmd = connection . CreateCommand ( ) ;
166181 cmd . CommandText = @"
167- SELECT server_id, object_name, counter_name, instance_name, cntr_value
182+ SELECT server_id, object_name, counter_name, instance_name, cntr_value, collection_time
168183FROM perfmon_stats
169184WHERE (server_id, collection_time) IN (
170185 SELECT server_id, MAX(collection_time) FROM perfmon_stats GROUP BY server_id
@@ -177,7 +192,8 @@ FROM perfmon_stats
177192 var objectName = reader . IsDBNull ( 1 ) ? "" : reader . GetString ( 1 ) ;
178193 var counter = reader . IsDBNull ( 2 ) ? "" : reader . GetString ( 2 ) ;
179194 var instance = reader . IsDBNull ( 3 ) ? "" : reader . GetString ( 3 ) ;
180- Seed ( serverId , "perfmon" , $ "{ objectName } |{ counter } |{ instance } ", reader . GetInt64 ( 4 ) ) ;
195+ var ts = reader . IsDBNull ( 5 ) ? ( DateTime ? ) null : reader . GetDateTime ( 5 ) ;
196+ Seed ( serverId , "perfmon" , $ "{ objectName } |{ counter } |{ instance } ", reader . GetInt64 ( 4 ) , ts ) ;
181197 count ++ ;
182198 }
183199 if ( count > 0 ) _logger ? . LogDebug ( "Seeded {Count} perfmon_stats baseline rows" , count ) ;
@@ -202,8 +218,8 @@ FROM memory_grant_stats
202218 var poolId = reader . IsDBNull ( 1 ) ? 0 : reader . GetInt32 ( 1 ) ;
203219 var semaphoreId = reader . IsDBNull ( 2 ) ? ( short ) 0 : reader . GetInt16 ( 2 ) ;
204220 var deltaKey = $ "{ poolId } _{ semaphoreId } ";
205- Seed ( serverId , "memory_grants_timeouts" , deltaKey , reader . IsDBNull ( 3 ) ? 0 : reader . GetInt64 ( 3 ) ) ;
206- Seed ( serverId , "memory_grants_forced" , deltaKey , reader . IsDBNull ( 4 ) ? 0 : reader . GetInt64 ( 4 ) ) ;
221+ Seed ( serverId , "memory_grants_timeouts" , deltaKey , reader . IsDBNull ( 3 ) ? 0 : reader . GetInt64 ( 3 ) , null ) ;
222+ Seed ( serverId , "memory_grants_forced" , deltaKey , reader . IsDBNull ( 4 ) ? 0 : reader . GetInt64 ( 4 ) , null ) ;
207223 count ++ ;
208224 }
209225 if ( count > 0 ) _logger ? . LogDebug ( "Seeded {Count} memory_grant_stats baseline rows" , count ) ;
0 commit comments