Skip to content

Commit 688b23a

Browse files
author
rhamlett_microsoft
committed
Another fix for slow requets
1 parent a92fc3d commit 688b23a

File tree

5 files changed

+202
-34
lines changed

5 files changed

+202
-34
lines changed

src/PerfProblemSimulator/Controllers/SlowRequestController.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,29 +173,33 @@ public IActionResult GetScenarios()
173173
/// HTTP endpoint that simulates a slow blocking request.
174174
/// </summary>
175175
/// <param name="durationSeconds">How long the request should take (default: 25 seconds).</param>
176+
/// <param name="scenario">The scenario name for logging (optional).</param>
176177
/// <returns>Result after the blocking delay completes.</returns>
177178
/// <remarks>
178179
/// <para>
179180
/// <strong>⚠️ This endpoint intentionally blocks!</strong>
180181
/// </para>
181182
/// <para>
182183
/// This endpoint is designed to be called during slow request simulation so that
183-
/// slow requests show up in the Request Latency Monitor chart. It uses sync-over-async
184-
/// to block the thread pool thread.
184+
/// slow requests show up in the Request Latency Monitor chart. It uses Thread.Sleep
185+
/// to block the ASP.NET thread pool thread.
185186
/// </para>
186187
/// </remarks>
187188
/// <response code="200">Request completed after blocking</response>
188189
[HttpGet("execute-slow")]
189190
[ProducesResponseType(typeof(SlowRequestResult), StatusCodes.Status200OK)]
190-
public IActionResult ExecuteSlowRequest([FromQuery] int durationSeconds = 25)
191+
public IActionResult ExecuteSlowRequest(
192+
[FromQuery] int durationSeconds = 25,
193+
[FromQuery] string? scenario = null)
191194
{
192195
var startTime = DateTimeOffset.UtcNow;
193196

194-
_logger.LogWarning("🐌 HTTP slow request started: {Duration}s", durationSeconds);
197+
_logger.LogWarning("🐌 HTTP slow request started: {Duration}s, Scenario: {Scenario}",
198+
durationSeconds, scenario ?? "Direct");
195199

196-
// BAD: Intentionally blocking with sync-over-async
197-
// This makes the request visible in the latency chart
198-
Task.Delay(durationSeconds * 1000).Wait();
200+
// BAD: Intentionally blocking with Thread.Sleep
201+
// This makes the request visible in the latency chart AND shows up clearly in CLR Profiler
202+
Thread.Sleep(durationSeconds * 1000);
199203

200204
var elapsed = DateTimeOffset.UtcNow - startTime;
201205

@@ -205,6 +209,7 @@ public IActionResult ExecuteSlowRequest([FromQuery] int durationSeconds = 25)
205209
{
206210
Message = $"Slow request completed after {elapsed.TotalSeconds:F1} seconds",
207211
DurationSeconds = elapsed.TotalSeconds,
212+
Scenario = scenario ?? "Direct",
208213
StartedAt = startTime,
209214
CompletedAt = DateTimeOffset.UtcNow
210215
});
@@ -218,6 +223,7 @@ public class SlowRequestResult
218223
{
219224
public string Message { get; set; } = "";
220225
public double DurationSeconds { get; set; }
226+
public string Scenario { get; set; } = "";
221227
public DateTimeOffset StartedAt { get; set; }
222228
public DateTimeOffset CompletedAt { get; set; }
223229
}

src/PerfProblemSimulator/Hubs/IMetricsClient.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,42 @@ public interface IMetricsClient
5555
/// </para>
5656
/// </remarks>
5757
Task ReceiveLatency(LatencyMeasurement measurement);
58+
59+
/// <summary>
60+
/// Receives slow request latency data from the server.
61+
/// </summary>
62+
/// <param name="data">The slow request latency data.</param>
63+
/// <remarks>
64+
/// <para>
65+
/// This is used to track actual slow request durations (typically 20-25 seconds)
66+
/// separately from the lightweight probe latency.
67+
/// </para>
68+
/// </remarks>
69+
Task ReceiveSlowRequestLatency(SlowRequestLatencyData data);
70+
}
71+
72+
/// <summary>
73+
/// Data about a slow request's latency.
74+
/// </summary>
75+
public class SlowRequestLatencyData
76+
{
77+
/// <summary>
78+
/// The request number in the simulation.
79+
/// </summary>
80+
public int RequestNumber { get; set; }
81+
82+
/// <summary>
83+
/// The scenario used for this request.
84+
/// </summary>
85+
public string Scenario { get; set; } = "";
86+
87+
/// <summary>
88+
/// The measured latency in milliseconds.
89+
/// </summary>
90+
public double LatencyMs { get; set; }
91+
92+
/// <summary>
93+
/// When this measurement was taken.
94+
/// </summary>
95+
public DateTimeOffset Timestamp { get; set; }
5896
}

src/PerfProblemSimulator/Program.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@
117117
// Educational Note: Singleton lifetime is required because the service maintains
118118
// state about running simulations and spawns background threads. This service
119119
// is designed to be used with CLR Profiler to demonstrate sync-over-async patterns.
120+
builder.Services.AddHttpClient("SlowRequest")
121+
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
122+
{
123+
// Long timeout for slow requests
124+
ConnectTimeout = TimeSpan.FromSeconds(60)
125+
});
120126
builder.Services.AddSingleton<ISlowRequestService, SlowRequestService>();
121127

122128
// MetricsCollector - Singleton service for collecting system metrics

src/PerfProblemSimulator/Services/SlowRequestService.cs

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
using Microsoft.AspNetCore.Hosting.Server;
2+
using Microsoft.AspNetCore.Hosting.Server.Features;
3+
using Microsoft.AspNetCore.SignalR;
4+
using PerfProblemSimulator.Hubs;
15
using PerfProblemSimulator.Models;
6+
using System.Diagnostics;
27
using System.Runtime.CompilerServices;
38

49
namespace 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.

src/PerfProblemSimulator/wwwroot/js/dashboard.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ const state = {
3636
isTimeout: [],
3737
isError: []
3838
},
39+
slowRequestHistory: {
40+
timestamps: [],
41+
values: [],
42+
scenarios: []
43+
},
3944
latencyStats: {
4045
timeoutCount: 0
4146
},
@@ -114,6 +119,8 @@ async function initializeSignalR() {
114119
state.connection.on('simulationCompleted', handleSimulationCompleted);
115120
state.connection.on('ReceiveLatency', handleLatencyUpdate);
116121
state.connection.on('receiveLatency', handleLatencyUpdate);
122+
state.connection.on('ReceiveSlowRequestLatency', handleSlowRequestLatency);
123+
state.connection.on('receiveSlowRequestLatency', handleSlowRequestLatency);
117124

118125
// Start connection
119126
try {
@@ -492,6 +499,40 @@ function handleLatencyUpdate(measurement) {
492499
updateLatencyChart();
493500
}
494501

502+
/**
503+
* Handle incoming slow request latency from server.
504+
* This shows the actual duration of slow requests (20+ seconds).
505+
*/
506+
function handleSlowRequestLatency(data) {
507+
console.log('🐌 Slow request latency:', data);
508+
509+
const timestamp = new Date(data.timestamp);
510+
const latencyMs = data.latencyMs;
511+
const scenario = data.scenario;
512+
513+
// Add to slow request history
514+
const history = state.slowRequestHistory;
515+
history.timestamps.push(timestamp);
516+
history.values.push(latencyMs);
517+
history.scenarios.push(scenario);
518+
519+
// Trim to max data points
520+
while (history.timestamps.length > 100) {
521+
history.timestamps.shift();
522+
history.values.shift();
523+
history.scenarios.shift();
524+
}
525+
526+
// Add as a special latency point on the chart (it will show as a large spike)
527+
addLatencyToHistory(timestamp, latencyMs, false, false);
528+
updateLatencyDisplay(latencyMs, false, false);
529+
updateLatencyChart();
530+
531+
// Log the slow request completion
532+
const durationSec = (latencyMs / 1000).toFixed(1);
533+
logEvent('warning', `🐌 Slow request #${data.requestNumber} completed: ${durationSec}s (${scenario})`);
534+
}
535+
495536
/**
496537
* Add latency measurement to history.
497538
*/

0 commit comments

Comments
 (0)