11using System ;
2- using System . Collections . Concurrent ;
32using System . Collections . Generic ;
43using System . Linq ;
54
65namespace SpacetimeDB
76{
7+ /// <summary>
8+ /// Class to track information about network requests and other internal statistics.
9+ /// </summary>
810 public class NetworkRequestTracker
911 {
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 ( ) ;
11106
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>
12116 private uint _nextRequestId ;
117+
118+ /// <summary>
119+ /// In-flight requests that have not yet finished running.
120+ /// </summary>
13121 private readonly Dictionary < uint , ( DateTime Start , string Metadata ) > _requests = new ( ) ;
14122
15123 internal uint StartTrackingRequest ( string metadata = "" )
16124 {
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+ }
21138 }
22139
140+ // The remaining methods in this class do not need to lock, since they are only called from OnProcessMessageComplete.
141+
23142 internal bool FinishTrackingRequest ( uint requestId )
24143 {
25144 if ( ! _requests . Remove ( requestId , out var entry ) )
@@ -42,28 +161,69 @@ internal bool FinishTrackingRequest(uint requestId)
42161
43162 internal void InsertRequest ( TimeSpan duration , string metadata )
44163 {
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+ }
46182 }
47183
48184 internal void InsertRequest ( DateTime start , string metadata )
49185 {
50186 InsertRequest ( DateTime . UtcNow - start , metadata ) ;
51187 }
52188
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 )
54201 {
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 ;
57203
58- if ( ! requestDurations . Any ( ) )
204+ if ( Trackers . TryGetValue ( lastSeconds , out var tracker ) )
59205 {
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 ) ) ;
61212 }
62213
63- return ( requestDurations . Min ( ) , requestDurations . Max ( ) ) ;
214+ return null ;
64215 }
65216
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>
67227 public int GetRequestsAwaitingResponse ( ) => _requests . Count ;
68228 }
69229
0 commit comments