@@ -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) < $end) { hash('sha256', $data); }</item>
859+ /// <item>Node.js: while (Date.now() < end) { crypto.createHash('sha256').update(data).digest(); }</item>
860+ /// <item>Java: while (System.nanoTime() < end) { MessageDigest.digest(data); }</item>
861+ /// <item>Python: while time.perf_counter() < 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
0 commit comments