Skip to content

Commit 149bdb1

Browse files
author
rhamlett_microsoft
committed
Modified load testing to better balance CPU and thread pool usage.
1 parent 5b1ac8e commit 149bdb1

2 files changed

Lines changed: 76 additions & 16 deletions

File tree

src/PerfProblemSimulator/Services/LoadTestService.cs

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -464,14 +464,18 @@ public async Task<LoadTestResult> ExecuteWorkAsync(LoadTestRequest request, Canc
464464
* concurrent request count. This GUARANTEES thread pool exhaustion
465465
* under any significant load.
466466
*
467-
* THREAD BLOCKING:
468-
* We use Thread.Sleep to BLOCK threads. At 500ms baseline with
469-
* high request rate, threads will exhaust rapidly.
467+
* THREAD BLOCKING WITH CPU WORK:
468+
* We use SpinWait (CPU-intensive) instead of Thread.Sleep to ensure
469+
* CPU usage scales with thread pool blocking. This creates balanced
470+
* stress across CPU, memory, and thread pool.
470471
*/
471472
if (request.BaselineDelayMs > 0)
472473
{
473-
_logger.LogDebug("Applying baseline blocking delay: {Delay}ms", request.BaselineDelayMs);
474-
Thread.Sleep(request.BaselineDelayMs);
474+
_logger.LogDebug(
475+
"Applying baseline blocking delay: {Delay}ms",
476+
request.BaselineDelayMs);
477+
478+
SpinWaitCpuIntensive(request.BaselineDelayMs);
475479
degradationDelayApplied += request.BaselineDelayMs;
476480

477481
// Check for timeout exception after baseline delay
@@ -510,10 +514,9 @@ public async Task<LoadTestResult> ExecuteWorkAsync(LoadTestRequest request, Canc
510514
* 1. Cancellation token (request aborted)
511515
* 2. Timeout threshold for exception throwing
512516
*
513-
* THREAD BLOCKING:
514-
* We use Thread.Sleep instead of Task.Delay to BLOCK threads.
515-
* This causes thread pool starvation under load, which is a realistic
516-
* simulation of poorly-written synchronous code in production apps.
517+
* THREAD BLOCKING WITH CPU WORK:
518+
* SpinWait burns CPU during delays, creating balanced stress where
519+
* CPU usage scales with blocked threads.
517520
*
518521
* WHY CHUNKS:
519522
* By chunking into 1s intervals, we can check for cancellation and
@@ -525,12 +528,14 @@ public async Task<LoadTestResult> ExecuteWorkAsync(LoadTestRequest request, Canc
525528
// Check for cancellation (request aborted by client)
526529
cancellationToken.ThrowIfCancellationRequested();
527530

528-
// Sleep for up to 1 second (or remaining delay, whichever is smaller)
529-
// Using Thread.Sleep to BLOCK the thread (causes thread pool starvation)
530-
var sleepMs = Math.Min(remainingDelay, ExceptionCheckIntervalMs);
531-
Thread.Sleep(sleepMs);
532-
remainingDelay -= sleepMs;
533-
degradationDelayApplied += sleepMs;
531+
// Wait for up to 1 second (or remaining delay, whichever is smaller)
532+
var delayMs = Math.Min(remainingDelay, ExceptionCheckIntervalMs);
533+
534+
// Burn CPU cycles during delay (balanced stress)
535+
SpinWaitCpuIntensive(delayMs);
536+
537+
remainingDelay -= delayMs;
538+
degradationDelayApplied += delayMs;
534539

535540
// Check for timeout exception trigger
536541
CheckAndThrowTimeoutException(stopwatch);
@@ -821,6 +826,61 @@ private byte[] AllocateAndUseMemory(int sizeBytes)
821826
return buffer;
822827
}
823828

829+
/*
830+
* =========================================================================
831+
* HELPER: CPU-Intensive Spin Wait
832+
* =========================================================================
833+
*
834+
* ALGORITHM:
835+
* targetEnd = now() + delayMs
836+
* while now() < targetEnd:
837+
* # Do CPU work instead of sleeping
838+
* hash = sha256(hash) # Burns CPU cycles
839+
*
840+
* WHY SPIN-WAIT INSTEAD OF THREAD.SLEEP:
841+
* Spin-wait burns CPU while blocking threads, creating balanced stress
842+
* where both CPU and thread pool show high utilization. Thread.Sleep
843+
* would only show thread pool exhaustion with 0% CPU during delays.
844+
*/
845+
846+
/// <summary>
847+
/// Performs a CPU-intensive wait for the specified duration.
848+
/// Unlike Thread.Sleep, this actively burns CPU cycles.
849+
/// </summary>
850+
/// <param name="milliseconds">Duration to spin-wait in milliseconds.</param>
851+
/// <remarks>
852+
/// <para>
853+
/// <strong>PORTING NOTES:</strong>
854+
/// </para>
855+
/// <para>
856+
/// Most languages have spin-wait or busy-wait constructs:
857+
/// <list type="bullet">
858+
/// <item>PHP: while (microtime(true) &lt; $end) { hash('sha256', $data); }</item>
859+
/// <item>Node.js: while (Date.now() &lt; end) { crypto.createHash('sha256').update(data).digest(); }</item>
860+
/// <item>Java: while (System.nanoTime() &lt; end) { MessageDigest.digest(data); }</item>
861+
/// <item>Python: while time.perf_counter() &lt; end: hashlib.sha256(data).digest()</item>
862+
/// </list>
863+
/// </para>
864+
/// </remarks>
865+
private void SpinWaitCpuIntensive(int milliseconds)
866+
{
867+
if (milliseconds <= 0) return;
868+
869+
var stopwatch = Stopwatch.StartNew();
870+
using var sha256 = SHA256.Create();
871+
var data = Encoding.UTF8.GetBytes($"SpinWait-{Guid.NewGuid()}");
872+
873+
// Spin until target duration reached
874+
while (stopwatch.ElapsedMilliseconds < milliseconds)
875+
{
876+
// Do CPU work to burn cycles
877+
data = sha256.ComputeHash(data);
878+
}
879+
880+
// Use result to prevent optimization
881+
_ = data.Length;
882+
}
883+
824884
/*
825885
* =========================================================================
826886
* HELPER: Build Result

src/PerfProblemSimulator/wwwroot/azure-monitoring-guide.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ <h3>Request Body Parameters Reference</h3>
995995
<tr>
996996
<td><code>baselineDelayMs</code></td>
997997
<td>500</td>
998-
<td>Minimum blocking delay (ms) for every request. Guarantees thread pool exhaustion.</td>
998+
<td>Minimum blocking delay (ms) for every request. Burns CPU while blocking threads.</td>
999999
</tr>
10001000
<tr>
10011001
<td><code>softLimit</code></td>

0 commit comments

Comments
 (0)