Skip to content

Commit 7335422

Browse files
authored
Faster Stats.cs (#326)
## Description of Changes `NetworkRequestTracker` previously was keeping all historical reducer request data, and searching through this every frame to get statistics. I've modified it to throw out much more data -- it's much faster now, but only updates every few seconds. ## API Not an API break, but deprecates an argument of one of NetworkRequestTracker's methods to no longer do anything. Adds new APIs. ## Requires SpacetimeDB PRs N/A ## Testsuite SpacetimeDB branch name: master ## Testing - [x] Tested Bitcraft. **Their F9 debug menu will require an update, since we now only keep one time window of request data, rather than being able to give information about multiple windows.** But it works. - [x] Blackholio CI
1 parent 685497a commit 7335422

File tree

1 file changed

+174
-14
lines changed

1 file changed

+174
-14
lines changed

src/Stats.cs

Lines changed: 174 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,144 @@
11
using System;
2-
using System.Collections.Concurrent;
32
using System.Collections.Generic;
43
using System.Linq;
54

65
namespace 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

Comments
 (0)