|
36 | 36 | * DEPENDENCIES: |
37 | 37 | * - Models/LoadTestRequest.cs - Input parameters |
38 | 38 | * - Models/LoadTestResult.cs - Output format |
39 | | - * - System.Security.Cryptography - For SHA256 hash work |
40 | 39 | * - System.Diagnostics - For Stopwatch timing |
| 40 | + * - System.Threading - For Thread.SpinWait CPU work |
41 | 41 | * |
42 | 42 | * ============================================================================= |
43 | 43 | */ |
|
46 | 46 | using PerfProblemSimulator.Hubs; |
47 | 47 | using PerfProblemSimulator.Models; |
48 | 48 | using System.Diagnostics; |
49 | | -using System.Security.Cryptography; |
50 | | -using System.Text; |
51 | 49 |
|
52 | 50 | namespace PerfProblemSimulator.Services; |
53 | 51 |
|
@@ -499,37 +497,36 @@ public async Task<LoadTestResult> ExecuteWorkAsync(LoadTestRequest request, Canc |
499 | 497 | * ================================================================= |
500 | 498 | * |
501 | 499 | * Instead of doing all CPU work at once then sleeping, we interleave: |
502 | | - * - Do ~50ms worth of CPU work (hash iterations) |
| 500 | + * - Do CPU work for cpuWorkMs milliseconds (spin loop) |
503 | 501 | * - Touch the memory buffer (keeps it active, prevents GC optimization) |
504 | 502 | * - Sleep briefly (~50ms) |
505 | 503 | * - Repeat until total duration reached |
506 | 504 | * |
507 | | - * This creates ~50% CPU utilization per active thread, allowing |
508 | | - * CPU to scale with concurrency without immediately hitting 100%. |
| 505 | + * This creates tunable CPU utilization per active thread: |
| 506 | + * - workIterations / 100 = ms of CPU work per cycle |
| 507 | + * - workIterations = 1000 → 10ms work + 50ms sleep = ~17% CPU per thread |
| 508 | + * - workIterations = 5000 → 50ms work + 50ms sleep = ~50% CPU per thread |
509 | 509 | * |
510 | 510 | * TUNING: |
511 | | - * - workIterations controls CPU intensity per cycle |
| 511 | + * - workIterations controls CPU intensity (divide by 100 for ms per cycle) |
512 | 512 | * - Higher workIterations = more CPU per cycle |
513 | | - * - 0 workIterations = pure thread blocking (minimal CPU) |
| 513 | + * - 0 workIterations = pure thread blocking (0% CPU) |
514 | 514 | */ |
515 | | - const int CycleMs = 100; // Each cycle is ~100ms (50ms work + 50ms sleep) |
516 | 515 | const int SleepPerCycleMs = 50; |
517 | 516 |
|
518 | | - // Calculate iterations per cycle to spread workIterations across duration |
519 | | - var totalCycles = Math.Max(1, totalDurationMs / CycleMs); |
520 | | - var iterationsPerCycle = request.WorkIterations > 0 |
521 | | - ? Math.Max(100, request.WorkIterations / totalCycles) |
522 | | - : 0; |
| 517 | + // Calculate CPU work time: workIterations / 100 = ms per cycle |
| 518 | + // Examples: 1000 → 10ms, 5000 → 50ms, 10000 → 100ms |
| 519 | + var cpuWorkMsPerCycle = request.WorkIterations / 100; |
523 | 520 |
|
524 | 521 | while (stopwatch.ElapsedMilliseconds < totalDurationMs) |
525 | 522 | { |
526 | 523 | cancellationToken.ThrowIfCancellationRequested(); |
527 | 524 |
|
528 | | - // CPU work phase |
529 | | - if (iterationsPerCycle > 0) |
| 525 | + // CPU work phase (spin loop for precise duration) |
| 526 | + if (cpuWorkMsPerCycle > 0) |
530 | 527 | { |
531 | | - PerformCpuWork(iterationsPerCycle); |
532 | | - totalCpuWorkDone += iterationsPerCycle; |
| 528 | + PerformCpuWork(cpuWorkMsPerCycle); |
| 529 | + totalCpuWorkDone += cpuWorkMsPerCycle; // Track total ms of CPU work |
533 | 530 | } |
534 | 531 |
|
535 | 532 | // Keep memory active (prevents GC from collecting early) |
@@ -670,55 +667,54 @@ private void CheckAndThrowTimeoutException(Stopwatch stopwatch) |
670 | 667 |
|
671 | 668 | /* |
672 | 669 | * ========================================================================= |
673 | | - * HELPER: Perform CPU Work |
| 670 | + * HELPER: Perform CPU Work (Time-Based) |
674 | 671 | * ========================================================================= |
675 | 672 | * |
676 | 673 | * ALGORITHM: |
677 | | - * data = "LoadTest-" + randomBytes |
678 | | - * for i in range(iterations): |
679 | | - * hash = SHA256(data) |
680 | | - * data = hash # Feed output back as input |
| 674 | + * start = now() |
| 675 | + * while (now() - start < workMs): |
| 676 | + * spinWait() # Busy loop consuming CPU |
681 | 677 | * |
682 | | - * This creates consistent, non-optimizable CPU work. |
| 678 | + * This creates consistent, measurable CPU consumption for a specified duration. |
683 | 679 | */ |
684 | 680 |
|
685 | 681 | /// <summary> |
686 | | - /// Performs CPU-intensive work by computing SHA256 hashes. |
| 682 | + /// Performs CPU-intensive work using a spin loop for the specified duration. |
687 | 683 | /// </summary> |
688 | | - /// <param name="iterations">Number of hash iterations to perform.</param> |
689 | | - private void PerformCpuWork(int iterations) |
| 684 | + /// <param name="workMs">Milliseconds of CPU work to perform.</param> |
| 685 | + private void PerformCpuWork(int workMs) |
690 | 686 | { |
691 | 687 | /* |
692 | 688 | * IMPLEMENTATION NOTES: |
693 | 689 | * |
694 | | - * We start with a seed value and repeatedly hash it. |
695 | | - * Each hash output becomes the input for the next iteration. |
696 | | - * This prevents the compiler from optimizing away the work. |
| 690 | + * We use a spin loop (busy wait) to consume CPU for a precise duration. |
| 691 | + * This is more predictable than hash iterations because: |
| 692 | + * - Hash speed varies by CPU, making iteration counts unreliable |
| 693 | + * - Time-based approach gives consistent CPU utilization |
697 | 694 | * |
698 | | - * SHA256 CHOICE: |
699 | | - * - Consistent performance across platforms |
700 | | - * - Available in all languages' standard libraries |
701 | | - * - Sufficient CPU load without being excessive |
| 695 | + * SPIN LOOP vs HASH: |
| 696 | + * - Spin loop gives precise time control |
| 697 | + * - Creates visible CPU load in monitoring tools |
| 698 | + * - SpinWait(1000) per iteration prevents compiler optimization |
702 | 699 | * |
703 | 700 | * PORTING: |
704 | | - * Replace SHA256 with your language's equivalent: |
705 | | - * - PHP: hash('sha256', $data, true) |
706 | | - * - Node.js: crypto.createHash('sha256').update(data).digest() |
707 | | - * - Java: MessageDigest.getInstance("SHA-256").digest(data) |
708 | | - * - Python: hashlib.sha256(data).digest() |
| 701 | + * Implement busy waiting in your language: |
| 702 | + * - PHP: while (microtime(true) * 1000 < endTime) { spin; } |
| 703 | + * - Node.js: while (Date.now() < endTime) { spin; } |
| 704 | + * - Java: while (System.currentTimeMillis() < endTime) { Thread.onSpinWait(); } |
| 705 | + * - Python: while time.time() * 1000 < end_time: pass |
709 | 706 | */ |
710 | | - using var sha256 = SHA256.Create(); |
711 | | - var data = Encoding.UTF8.GetBytes($"LoadTest-{Guid.NewGuid()}"); |
| 707 | + if (workMs <= 0) return; |
712 | 708 |
|
713 | | - for (var i = 0; i < iterations; i++) |
| 709 | + var sw = Stopwatch.StartNew(); |
| 710 | + while (sw.ElapsedMilliseconds < workMs) |
714 | 711 | { |
715 | | - data = sha256.ComputeHash(data); |
| 712 | + // SpinWait burns CPU cycles without yielding to OS scheduler |
| 713 | + // 1000 iterations is ~1-2 microseconds, creates tight loop |
| 714 | + Thread.SpinWait(1000); |
716 | 715 | } |
717 | | - |
718 | | - // Use the result to prevent optimization |
719 | | - // (compiler can't remove work if result is used) |
720 | | - _ = data.Length; |
721 | 716 | } |
| 717 | + |
722 | 718 | /* |
723 | 719 | * ========================================================================= |
724 | 720 | * HELPER: Build Result |
@@ -871,7 +867,7 @@ private void UpdateMaxResponseTime(long responseTimeMs) |
871 | 867 | * Use atomic increment/decrement for concurrent request tracking |
872 | 868 | * |
873 | 869 | * 4. CPU Work: |
874 | | - * Repeated SHA256 hashing with output fed back as input |
| 870 | + * Time-based spin loop (workIterations / 100 = ms per cycle) |
875 | 871 | * |
876 | 872 | * 5. Memory Allocation: |
877 | 873 | * Allocate buffer and write pattern to force actual allocation |
|
0 commit comments