1+ using Microsoft . AspNetCore . Hosting . Server ;
2+ using Microsoft . AspNetCore . Hosting . Server . Features ;
3+ using Microsoft . AspNetCore . SignalR ;
4+ using PerfProblemSimulator . Hubs ;
15using PerfProblemSimulator . Models ;
6+ using System . Diagnostics ;
27using System . Runtime . CompilerServices ;
38
49namespace PerfProblemSimulator . Services ;
@@ -37,6 +42,9 @@ public class SlowRequestService : ISlowRequestService, IDisposable
3742{
3843 private readonly ISimulationTracker _simulationTracker ;
3944 private readonly ILogger < SlowRequestService > _logger ;
45+ private readonly IHttpClientFactory _httpClientFactory ;
46+ private readonly IHubContext < MetricsHub , IMetricsClient > _hubContext ;
47+ private readonly IServer _server ;
4048 private readonly Random _random = new ( ) ;
4149
4250 private CancellationTokenSource ? _cts ;
@@ -52,15 +60,22 @@ public class SlowRequestService : ISlowRequestService, IDisposable
5260 private Guid _simulationId ;
5361 private readonly Dictionary < string , int > _scenarioCounts = new ( ) ;
5462 private readonly object _lock = new ( ) ;
63+ private string ? _baseUrl ;
5564
5665 public bool IsRunning => _isRunning ;
5766
5867 public SlowRequestService (
5968 ISimulationTracker simulationTracker ,
60- ILogger < SlowRequestService > logger )
69+ ILogger < SlowRequestService > logger ,
70+ IHttpClientFactory httpClientFactory ,
71+ IHubContext < MetricsHub , IMetricsClient > hubContext ,
72+ IServer server )
6173 {
6274 _simulationTracker = simulationTracker ?? throw new ArgumentNullException ( nameof ( simulationTracker ) ) ;
6375 _logger = logger ?? throw new ArgumentNullException ( nameof ( logger ) ) ;
76+ _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException ( nameof ( httpClientFactory ) ) ;
77+ _hubContext = hubContext ?? throw new ArgumentNullException ( nameof ( hubContext ) ) ;
78+ _server = server ?? throw new ArgumentNullException ( nameof ( server ) ) ;
6479 }
6580
6681 public SimulationResult Start ( SlowRequestRequest request )
@@ -81,12 +96,15 @@ public SimulationResult Start(SlowRequestRequest request)
8196 _requestsSent = 0 ;
8297 _requestsCompleted = 0 ;
8398 _requestsInProgress = 0 ;
84- _intervalSeconds = Math . Max ( 5 , request . IntervalSeconds ) ;
85- _requestDurationSeconds = Math . Max ( 10 , request . RequestDurationSeconds ) ;
99+ _intervalSeconds = Math . Max ( 1 , request . IntervalSeconds ) ; // Allow 1 second minimum
100+ _requestDurationSeconds = Math . Max ( 5 , request . RequestDurationSeconds ) ; // Allow 5 second minimum
86101 _maxRequests = request . MaxRequests ;
87102 _startedAt = DateTimeOffset . UtcNow ;
88103 _scenarioCounts . Clear ( ) ;
89104 _isRunning = true ;
105+
106+ // Get the base URL for HTTP calls
107+ _baseUrl = GetBaseUrl ( ) ;
90108
91109 var parameters = new Dictionary < string , object >
92110 {
@@ -108,8 +126,8 @@ public SimulationResult Start(SlowRequestRequest request)
108126 _logger . LogWarning (
109127 "🐌 Slow request simulation started: {SimulationId}. " +
110128 "Duration={Duration}s, Interval={Interval}s. " +
111- "Requests will use random sync-over-async scenarios. " ,
112- _simulationId , _requestDurationSeconds , _intervalSeconds ) ;
129+ "Making HTTP calls to {BaseUrl}/api/slowrequest/execute-slow " ,
130+ _simulationId , _requestDurationSeconds , _intervalSeconds , _baseUrl ) ;
113131
114132 return new SimulationResult
115133 {
@@ -190,7 +208,7 @@ private void SpawnRequestsLoop(int maxRequests, CancellationToken ct)
190208 Interlocked . Increment ( ref _requestsSent ) ;
191209 Interlocked . Increment ( ref _requestsInProgress ) ;
192210
193- // Randomly select a scenario
211+ // Randomly select a scenario for logging purposes
194212 var scenario = ( SlowRequestScenario ) _random . Next ( 1 , 4 ) ; // 1, 2, or 3 (skip Random=0)
195213
196214 lock ( _lock )
@@ -201,11 +219,12 @@ private void SpawnRequestsLoop(int maxRequests, CancellationToken ct)
201219 }
202220
203221 _logger . LogInformation (
204- "🐌 Spawning slow request #{Number} using scenario: {Scenario }" ,
205- requestNumber , scenario ) ;
222+ "🐌 Spawning slow HTTP request #{Number} to {BaseUrl}/api/slowrequest/execute-slow?durationSeconds={Duration }" ,
223+ requestNumber , _baseUrl , _requestDurationSeconds ) ;
206224
207- // Spawn the slow request on a new thread (simulating incoming HTTP request)
208- var requestThread = new Thread ( ( ) => ExecuteSlowRequest ( scenario , requestNumber , _requestDurationSeconds , ct ) )
225+ // Spawn the slow request on a new thread - this will make an HTTP call
226+ var reqNum = requestNumber ;
227+ var requestThread = new Thread ( ( ) => ExecuteSlowHttpRequest ( scenario , reqNum , _requestDurationSeconds , ct ) )
209228 {
210229 Name = $ "SlowRequest-{ requestNumber } -{ scenario } ",
211230 IsBackground = true
@@ -226,36 +245,47 @@ private void SpawnRequestsLoop(int maxRequests, CancellationToken ct)
226245 _logger . LogInformation ( "Slow request spawner loop ended" ) ;
227246 }
228247
229- private void ExecuteSlowRequest ( SlowRequestScenario scenario , int requestNumber , int durationSeconds , CancellationToken ct )
248+ /// <summary>
249+ /// Makes an HTTP call to the slow request endpoint - this goes through the ASP.NET pipeline
250+ /// and will show up in the Request Latency Monitor.
251+ /// </summary>
252+ private void ExecuteSlowHttpRequest ( SlowRequestScenario scenario , int requestNumber , int durationSeconds , CancellationToken ct )
230253 {
254+ var sw = Stopwatch . StartNew ( ) ;
231255 try
232256 {
233- // Execute the blocking scenario - uses Thread.Sleep so it shows up in CLR Profiler
234- switch ( scenario )
257+ if ( string . IsNullOrEmpty ( _baseUrl ) )
235258 {
236- case SlowRequestScenario . SimpleSyncOverAsync :
237- ExecuteSimpleSyncOverAsyncRequest ( durationSeconds , ct ) ;
238- break ;
239-
240- case SlowRequestScenario . NestedSyncOverAsync :
241- ExecuteNestedSyncOverAsyncRequest ( durationSeconds , ct ) ;
242- break ;
243-
244- case SlowRequestScenario . DatabasePattern :
245- ExecuteDatabasePatternRequest ( durationSeconds , ct ) ;
246- break ;
259+ _logger . LogError ( "Base URL not configured for slow request HTTP calls" ) ;
260+ return ;
247261 }
248262
249- _logger . LogInformation ( "🐌 Slow request #{Number} ({Scenario}) completed after {Duration}s" ,
250- requestNumber , scenario , durationSeconds ) ;
263+ var client = _httpClientFactory . CreateClient ( "SlowRequest" ) ;
264+ client . Timeout = TimeSpan . FromSeconds ( durationSeconds + 30 ) ; // Extra buffer for network overhead
265+
266+ var url = $ "{ _baseUrl } /api/slowrequest/execute-slow?durationSeconds={ durationSeconds } &scenario={ scenario } ";
267+
268+ // Make HTTP call - this blocks the thread and goes through ASP.NET pipeline
269+ var response = client . GetAsync ( url , ct ) . GetAwaiter ( ) . GetResult ( ) ;
270+
271+ sw . Stop ( ) ;
272+ var latencyMs = sw . Elapsed . TotalMilliseconds ;
273+
274+ _logger . LogInformation (
275+ "🐌 Slow HTTP request #{Number} ({Scenario}) completed in {Latency:F0}ms (expected ~{Expected}s)" ,
276+ requestNumber , scenario , latencyMs , durationSeconds ) ;
277+
278+ // Broadcast the slow request latency to the dashboard
279+ BroadcastSlowRequestLatency ( requestNumber , scenario , latencyMs ) ;
251280 }
252281 catch ( OperationCanceledException )
253282 {
254283 _logger . LogDebug ( "Slow request #{Number} cancelled" , requestNumber ) ;
255284 }
256285 catch ( Exception ex )
257286 {
258- _logger . LogError ( ex , "Slow request #{Number} failed" , requestNumber ) ;
287+ sw . Stop ( ) ;
288+ _logger . LogError ( ex , "Slow HTTP request #{Number} failed after {Elapsed}ms" , requestNumber , sw . ElapsedMilliseconds ) ;
259289 }
260290 finally
261291 {
@@ -266,6 +296,53 @@ private void ExecuteSlowRequest(SlowRequestScenario scenario, int requestNumber,
266296 CheckAndCompleteSimulation ( ) ;
267297 }
268298 }
299+
300+ /// <summary>
301+ /// Broadcasts slow request latency to connected dashboard clients.
302+ /// </summary>
303+ private void BroadcastSlowRequestLatency ( int requestNumber , SlowRequestScenario scenario , double latencyMs )
304+ {
305+ try
306+ {
307+ _hubContext . Clients . All . ReceiveSlowRequestLatency ( new SlowRequestLatencyData
308+ {
309+ RequestNumber = requestNumber ,
310+ Scenario = scenario . ToString ( ) ,
311+ LatencyMs = latencyMs ,
312+ Timestamp = DateTimeOffset . UtcNow
313+ } ) . GetAwaiter ( ) . GetResult ( ) ;
314+ }
315+ catch ( Exception ex )
316+ {
317+ _logger . LogDebug ( ex , "Failed to broadcast slow request latency" ) ;
318+ }
319+ }
320+
321+ /// <summary>
322+ /// Gets the base URL for making HTTP calls to ourselves.
323+ /// </summary>
324+ private string ? GetBaseUrl ( )
325+ {
326+ try
327+ {
328+ var addresses = _server . Features . Get < IServerAddressesFeature > ( ) ;
329+ if ( addresses != null && addresses . Addresses . Count > 0 )
330+ {
331+ var address = addresses . Addresses . First ( ) ;
332+ // Replace wildcard addresses
333+ address = address . Replace ( "[::]" , "localhost" ) . Replace ( "0.0.0.0" , "localhost" ) ;
334+ _logger . LogDebug ( "Detected server address for slow requests: {Address}" , address ) ;
335+ return address ;
336+ }
337+ }
338+ catch ( Exception ex )
339+ {
340+ _logger . LogWarning ( ex , "Could not detect server address" ) ;
341+ }
342+
343+ // Fallback for Azure/production environments - use relative URL
344+ return "http://localhost:5021" ;
345+ }
269346
270347 /// <summary>
271348 /// Checks if all requests have completed and marks the simulation as done.
0 commit comments