@@ -84,7 +84,7 @@ public CpuStressService(
8484 }
8585
8686 /// <inheritdoc />
87- public Task < SimulationResult > TriggerCpuStressAsync ( int durationSeconds , CancellationToken cancellationToken )
87+ public Task < SimulationResult > TriggerCpuStressAsync ( int durationSeconds , CancellationToken cancellationToken , int targetPercentage = 100 )
8888 {
8989 // ==========================================================================
9090 // STEP 1: Validate the duration (no upper limits - app is meant to break)
@@ -93,6 +93,8 @@ public Task<SimulationResult> TriggerCpuStressAsync(int durationSeconds, Cancell
9393 ? DefaultDurationSeconds
9494 : durationSeconds ;
9595
96+ var actualPercentage = Math . Clamp ( targetPercentage , 1 , 100 ) ;
97+
9698 var simulationId = Guid . NewGuid ( ) ;
9799 var startedAt = DateTimeOffset . UtcNow ;
98100 var estimatedEndAt = startedAt . AddSeconds ( actualDuration ) ;
@@ -108,16 +110,18 @@ public Task<SimulationResult> TriggerCpuStressAsync(int durationSeconds, Cancell
108110 var parameters = new Dictionary < string , object >
109111 {
110112 [ "DurationSeconds" ] = actualDuration ,
111- [ "ProcessorCount" ] = processorCount
113+ [ "ProcessorCount" ] = processorCount ,
114+ [ "TargetPercentage" ] = actualPercentage
112115 } ;
113116
114117 // Register this simulation with the tracker
115118 _simulationTracker . RegisterSimulation ( simulationId , SimulationType . Cpu , parameters , cts ) ;
116119
117120 _logger . LogInformation (
118- "Starting CPU stress simulation {SimulationId}: {Duration}s across {ProcessorCount} cores" ,
121+ "Starting CPU stress simulation {SimulationId}: {Duration}s @ {Percentage}% across {ProcessorCount} cores" ,
119122 simulationId ,
120123 actualDuration ,
124+ actualPercentage ,
121125 processorCount ) ;
122126
123127 // ==========================================================================
@@ -128,7 +132,7 @@ public Task<SimulationResult> TriggerCpuStressAsync(int durationSeconds, Cancell
128132 // This is important because the caller (HTTP request) shouldn't be blocked
129133 // waiting for the entire duration.
130134
131- _ = Task . Run ( ( ) => ExecuteCpuStress ( simulationId , actualDuration , cts . Token ) , cts . Token ) ;
135+ _ = Task . Run ( ( ) => ExecuteCpuStress ( simulationId , actualDuration , actualPercentage , cts . Token ) , cts . Token ) ;
132136
133137 // ==========================================================================
134138 // STEP 4: Return the result immediately
@@ -141,7 +145,7 @@ public Task<SimulationResult> TriggerCpuStressAsync(int durationSeconds, Cancell
141145 SimulationId = simulationId ,
142146 Type = SimulationType . Cpu ,
143147 Status = "Started" ,
144- Message = $ "CPU stress started on { processorCount } cores for { actualDuration } seconds. " +
148+ Message = $ "CPU stress started on { processorCount } cores for { actualDuration } seconds at { actualPercentage } % . " +
145149 "Observe CPU metrics in Task Manager, dotnet-counters, or Application Insights. " +
146150 "High CPU like this is typically caused by spin loops, inefficient algorithms, or infinite loops." ,
147151 ActualParameters = parameters ,
@@ -160,9 +164,9 @@ public Task<SimulationResult> TriggerCpuStressAsync(int durationSeconds, Cancell
160164 /// <strong>⚠️ THIS IS AN ANTI-PATTERN - FOR EDUCATIONAL PURPOSES ONLY ⚠️</strong>
161165 /// </para>
162166 /// <para>
163- /// This method uses dedicated threads with spin loops to consume all available CPU cores .
164- /// Each thread runs a tight <c>while</c> loop that does nothing but check the time
165- /// and cancellation token .
167+ /// This method uses dedicated threads with spin loops to consume available CPU.
168+ /// If targetPercentage is 100, it runs a tight loop.
169+ /// If targetPercentage is less than 100, it uses a duty cycle (work/sleep) to simulate load .
166170 /// </para>
167171 /// <para>
168172 /// <strong>Why Dedicated Threads Instead of Parallel.For?</strong>
@@ -171,7 +175,7 @@ public Task<SimulationResult> TriggerCpuStressAsync(int durationSeconds, Cancell
171175 /// thread pool remains available for the dashboard and metrics collection.
172176 /// </para>
173177 /// </remarks>
174- private void ExecuteCpuStress ( Guid simulationId , int durationSeconds , CancellationToken cancellationToken )
178+ private void ExecuteCpuStress ( Guid simulationId , int durationSeconds , int targetPercentage , CancellationToken cancellationToken )
175179 {
176180 try
177181 {
@@ -182,27 +186,49 @@ private void ExecuteCpuStress(Guid simulationId, int durationSeconds, Cancellati
182186 // ==========================================================================
183187 // THE ANTI-PATTERN: Dedicated thread spin loops
184188 // ==========================================================================
185- // This code intentionally:
186- // 1. Creates one dedicated thread per CPU core
187- // 2. Each thread runs a tight while loop (spin loop)
188- // 3. The while loop continuously checks the time and does NOTHING useful
189- //
190- // We use dedicated threads instead of Parallel.For to avoid starving the
191- // thread pool, which would freeze the dashboard and SignalR.
192-
189+ // This code intentionally creates one dedicated thread per CPU core.
190+
193191 var threads = new Thread [ processorCount ] ;
194192
195193 for ( int i = 0 ; i < processorCount ; i ++ )
196194 {
197195 var threadIndex = i ;
198196 threads [ i ] = new Thread ( ( ) =>
199197 {
200- // This spin loop is the source of high CPU usage
201- // It does nothing but burn CPU cycles checking conditions
202- while ( Stopwatch . GetTimestamp ( ) < endTime && ! cancellationToken . IsCancellationRequested )
198+ if ( targetPercentage >= 99 )
203199 {
204- // Intentionally empty - this is a spin loop
205- // Every CPU cycle spent here is a wasted cycle
200+ // 100% Load: Tight spin loop (simplest, most effective for maxing out core)
201+ while ( Stopwatch . GetTimestamp ( ) < endTime && ! cancellationToken . IsCancellationRequested )
202+ {
203+ // Intentionally empty - this is a spin loop
204+ }
205+ }
206+ else
207+ {
208+ // Partial Load: Duty Cycle
209+ // Work for X ms, Sleep for Y ms
210+ // Using a small window (e.g., 50ms) keeps usage relatively smooth
211+ const int windowMs = 50 ;
212+ int workMs = ( windowMs * targetPercentage ) / 100 ;
213+ int sleepMs = windowMs - workMs ;
214+
215+ while ( Stopwatch . GetTimestamp ( ) < endTime && ! cancellationToken . IsCancellationRequested )
216+ {
217+ var cycleStart = Stopwatch . GetTimestamp ( ) ;
218+ var workTicks = ( workMs * Stopwatch . Frequency ) / 1000 ;
219+
220+ // Spin for work portion
221+ while ( Stopwatch . GetTimestamp ( ) - cycleStart < workTicks )
222+ {
223+ // Spin
224+ }
225+
226+ // Sleep for remainder of window
227+ if ( sleepMs > 0 )
228+ {
229+ Thread . Sleep ( sleepMs ) ;
230+ }
231+ }
206232 }
207233 } )
208234 {
0 commit comments