1
1
using System ;
2
- using System . Collections . Concurrent ;
3
2
using System . Collections . Generic ;
4
3
using System . Linq ;
5
4
6
5
namespace SpacetimeDB
7
6
{
7
+ /// <summary>
8
+ /// Class to track information about network requests and other internal statistics.
9
+ /// </summary>
8
10
public class NetworkRequestTracker
9
11
{
10
- private readonly ConcurrentQueue < ( DateTime End , TimeSpan Duration , string Metadata ) > _requestDurations = new ( ) ;
12
+ public NetworkRequestTracker ( )
13
+ {
14
+ }
15
+
16
+ /// <summary>
17
+ /// The fastest request OF ALL TIME.
18
+ /// We keep data for less time than we used to -- having this around catches outliers that may be problematic.
19
+ /// </summary>
20
+ public ( TimeSpan Duration , string Metadata ) ? AllTimeMin
21
+ {
22
+ get ; private set ;
23
+ }
24
+
25
+ /// <summary>
26
+ /// The slowest request OF ALL TIME.
27
+ /// We keep data for less time than we used to -- having this around catches outliers that may be problematic.
28
+ /// </summary>
29
+ public ( TimeSpan Duration , string Metadata ) ? AllTimeMax
30
+ {
31
+ get ; private set ;
32
+ }
33
+
34
+ private int _totalSamples = 0 ;
35
+
36
+ /// <summary>
37
+ /// The maximum number of windows we are willing to track data in.
38
+ /// </summary>
39
+ public static readonly int MAX_TRACKERS = 16 ;
40
+
41
+ /// <summary>
42
+ /// A tracker that tracks the minimum and maximum sample in a time window,
43
+ /// resetting after <c>windowSeconds</c> seconds.
44
+ /// </summary>
45
+ private struct Tracker
46
+ {
47
+ public Tracker ( int windowSeconds )
48
+ {
49
+ LastReset = DateTime . UtcNow ;
50
+ Window = new TimeSpan ( 0 , 0 , windowSeconds ) ;
51
+ LastWindowMin = null ;
52
+ LastWindowMax = null ;
53
+ ThisWindowMin = null ;
54
+ ThisWindowMax = null ;
55
+ }
56
+
57
+ private DateTime LastReset ;
58
+ private TimeSpan Window ;
59
+
60
+ // The min and max for the previous window.
61
+ private ( TimeSpan Duration , string Metadata ) ? LastWindowMin ;
62
+ private ( TimeSpan Duration , string Metadata ) ? LastWindowMax ;
63
+
64
+ // The min and max for the current window.
65
+ private ( TimeSpan Duration , string Metadata ) ? ThisWindowMin ;
66
+ private ( TimeSpan Duration , string Metadata ) ? ThisWindowMax ;
67
+
68
+ public void InsertRequest ( TimeSpan duration , string metadata )
69
+ {
70
+ var sample = ( duration , metadata ) ;
71
+
72
+ if ( ThisWindowMin == null || ThisWindowMin . Value . Duration > duration )
73
+ {
74
+ ThisWindowMin = sample ;
75
+ }
76
+ if ( ThisWindowMax == null || ThisWindowMax . Value . Duration < duration )
77
+ {
78
+ ThisWindowMax = sample ;
79
+ }
80
+
81
+ if ( LastReset < DateTime . UtcNow - Window )
82
+ {
83
+ LastReset = DateTime . UtcNow ;
84
+ LastWindowMax = ThisWindowMax ;
85
+ LastWindowMin = ThisWindowMin ;
86
+ ThisWindowMax = null ;
87
+ ThisWindowMin = null ;
88
+ }
89
+ }
90
+
91
+ public ( ( TimeSpan Duration , string Metadata ) Min , ( TimeSpan Duration , string Metadata ) Max ) ? GetMinMaxTimes ( )
92
+ {
93
+ if ( LastWindowMin != null && LastWindowMax != null )
94
+ {
95
+ return ( LastWindowMin . Value , LastWindowMax . Value ) ;
96
+ }
97
+
98
+ return null ;
99
+ }
100
+ }
101
+
102
+ /// <summary>
103
+ /// Maps (requested window time in seconds) -> (the tracker for that time window).
104
+ /// </summary>
105
+ private readonly Dictionary < int , Tracker > Trackers = new ( ) ;
11
106
107
+ /// <summary>
108
+ /// To allow modifying Trackers in a loop.
109
+ /// This is needed because we made Tracker a struct.
110
+ /// </summary>
111
+ private readonly HashSet < int > TrackerWindows = new ( ) ;
112
+
113
+ /// <summary>
114
+ /// ID for the next in-flight request.
115
+ /// </summary>
12
116
private uint _nextRequestId ;
117
+
118
+ /// <summary>
119
+ /// In-flight requests that have not yet finished running.
120
+ /// </summary>
13
121
private readonly Dictionary < uint , ( DateTime Start , string Metadata ) > _requests = new ( ) ;
14
122
15
123
internal uint StartTrackingRequest ( string metadata = "" )
16
124
{
17
- // Record the start time of the request
18
- var newRequestId = ++ _nextRequestId ;
19
- _requests [ newRequestId ] = ( DateTime . UtcNow , metadata ) ;
20
- return newRequestId ;
125
+ // This method is called when the user submits a new request.
126
+ // It's possible the user was naughty and did this off the main thread.
127
+ // So, be a little paranoid and lock ourselves. Uncontended this will be pretty fast.
128
+ lock ( this )
129
+ {
130
+ // Get a new request ID.
131
+ // Note: C# wraps by default, rather than throwing exception on overflow.
132
+ // So, this class should work forever.
133
+ var newRequestId = ++ _nextRequestId ;
134
+ // Record the start time of the request.
135
+ _requests [ newRequestId ] = ( DateTime . UtcNow , metadata ) ;
136
+ return newRequestId ;
137
+ }
21
138
}
22
139
140
+ // The remaining methods in this class do not need to lock, since they are only called from OnProcessMessageComplete.
141
+
23
142
internal bool FinishTrackingRequest ( uint requestId )
24
143
{
25
144
if ( ! _requests . Remove ( requestId , out var entry ) )
@@ -42,28 +161,69 @@ internal bool FinishTrackingRequest(uint requestId)
42
161
43
162
internal void InsertRequest ( TimeSpan duration , string metadata )
44
163
{
45
- _requestDurations . Enqueue ( ( DateTime . UtcNow , duration , metadata ) ) ;
164
+ var sample = ( duration , metadata ) ;
165
+
166
+ if ( AllTimeMin == null || AllTimeMin . Value . Duration > duration )
167
+ {
168
+ AllTimeMin = sample ;
169
+ }
170
+ if ( AllTimeMax == null || AllTimeMax . Value . Duration < duration )
171
+ {
172
+ AllTimeMax = sample ;
173
+ }
174
+ _totalSamples += 1 ;
175
+
176
+ foreach ( var window in TrackerWindows )
177
+ {
178
+ var tracker = Trackers [ window ] ;
179
+ tracker . InsertRequest ( duration , metadata ) ;
180
+ Trackers [ window ] = tracker ; // Needed because struct.
181
+ }
46
182
}
47
183
48
184
internal void InsertRequest ( DateTime start , string metadata )
49
185
{
50
186
InsertRequest ( DateTime . UtcNow - start , metadata ) ;
51
187
}
52
188
53
- public ( ( TimeSpan Duration , string Metadata ) Min , ( TimeSpan Duration , string Metadata ) Max ) ? GetMinMaxTimes ( int lastSeconds )
189
+ /// <summary>
190
+ /// Get the the minimum- and maximum-duration events in lastSeconds.
191
+ /// When first called, this will return null until `lastSeconds` have passed.
192
+ /// After this, the value will update every `lastSeconds`.
193
+ ///
194
+ /// This class allocates an internal data structure for every distinct value of `lastSeconds` passed.
195
+ /// After `NetworkRequestTracker.MAX_TRACKERS` distinct values have been passed, it will stop allocating internal data structures
196
+ /// and always return null.
197
+ /// This should be fine as long as you don't request a large number of distinct windows.
198
+ /// </summary>
199
+ /// <param name="_deprecated">Present for backwards-compatibility, does nothing.</param>
200
+ public ( ( TimeSpan Duration , string Metadata ) Min , ( TimeSpan Duration , string Metadata ) Max ) ? GetMinMaxTimes ( int lastSeconds = 0 )
54
201
{
55
- var cutoff = DateTime . UtcNow . AddSeconds ( - lastSeconds ) ;
56
- var requestDurations = _requestDurations . Where ( x => x . End >= cutoff ) . Select ( x => ( x . Duration , x . Metadata ) ) ;
202
+ if ( lastSeconds <= 0 ) return null ;
57
203
58
- if ( ! requestDurations . Any ( ) )
204
+ if ( Trackers . TryGetValue ( lastSeconds , out var tracker ) )
59
205
{
60
- return null ;
206
+ return tracker . GetMinMaxTimes ( ) ;
207
+ }
208
+ else if ( TrackerWindows . Count < MAX_TRACKERS )
209
+ {
210
+ TrackerWindows . Add ( lastSeconds ) ;
211
+ Trackers . Add ( lastSeconds , new Tracker ( lastSeconds ) ) ;
61
212
}
62
213
63
- return ( requestDurations . Min ( ) , requestDurations . Max ( ) ) ;
214
+ return null ;
64
215
}
65
216
66
- public int GetSampleCount ( ) => _requestDurations . Count ;
217
+ /// <summary>
218
+ /// Get the number of samples in the window.
219
+ /// </summary>
220
+ /// <returns></returns>
221
+ public int GetSampleCount ( ) => _totalSamples ;
222
+
223
+ /// <summary>
224
+ /// Get the number of outstanding tracked requests.
225
+ /// </summary>
226
+ /// <returns></returns>
67
227
public int GetRequestsAwaitingResponse ( ) => _requests . Count ;
68
228
}
69
229
0 commit comments