Skip to content

Commit 44da74a

Browse files
author
rhamlett_microsoft
committed
Added ability to set target CPU percentage for CPU stress tests and improved UI feedback during slow request simulations.
1 parent 7b559ac commit 44da74a

File tree

6 files changed

+81
-30
lines changed

6 files changed

+81
-30
lines changed

src/PerfProblemSimulator/Controllers/CpuController.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,21 +103,24 @@ public async Task<IActionResult> TriggerHighCpu(
103103
{
104104
// Use defaults if no request body provided
105105
var durationSeconds = request?.DurationSeconds ?? 30;
106+
var targetPercentage = request?.TargetPercentage ?? 100;
106107

107108
// Log the incoming request (FR-010: Request logging)
108109
_logger.LogInformation(
109-
"Received CPU stress request: DurationSeconds={Duration}, ClientIP={ClientIP}",
110+
"Received CPU stress request: DurationSeconds={Duration}, Percentage={Percentage}, ClientIP={ClientIP}",
110111
durationSeconds,
112+
targetPercentage,
111113
HttpContext.Connection.RemoteIpAddress);
112114

113115
try
114116
{
115-
var result = await _cpuStressService.TriggerCpuStressAsync(durationSeconds, cancellationToken);
117+
var result = await _cpuStressService.TriggerCpuStressAsync(durationSeconds, cancellationToken, targetPercentage);
116118

117119
_logger.LogInformation(
118-
"Started CPU stress simulation {SimulationId} for {Duration}s",
120+
"Started CPU stress simulation {SimulationId} for {Duration}s @ {Percentage}%",
119121
result.SimulationId,
120-
result.ActualParameters?["DurationSeconds"]);
122+
result.ActualParameters?["DurationSeconds"],
123+
result.ActualParameters?["TargetPercentage"]);
121124

122125
return Ok(result);
123126
}

src/PerfProblemSimulator/Models/CpuStressRequest.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,16 @@ public class CpuStressRequest
3535
/// </remarks>
3636
[Range(1, int.MaxValue, ErrorMessage = "Duration must be at least 1 second")]
3737
public int DurationSeconds { get; set; } = 30;
38+
39+
/// <summary>
40+
/// The target CPU usage percentage (1-100).
41+
/// </summary>
42+
/// <remarks>
43+
/// <para>
44+
/// Default: 100%. Lower values use a duty cycle (work/sleep) to simulate
45+
/// partial CPU load.
46+
/// </para>
47+
/// </remarks>
48+
[Range(1, 100, ErrorMessage = "Target percentage must be between 1 and 100")]
49+
public int TargetPercentage { get; set; } = 100;
3850
}

src/PerfProblemSimulator/Services/CpuStressService.cs

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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
{

src/PerfProblemSimulator/Services/ICpuStressService.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public interface ICpuStressService
2626
/// <param name="cancellationToken">
2727
/// Token to request early cancellation of the stress operation.
2828
/// </param>
29+
/// <param name="targetPercentage">
30+
/// Target CPU usage percentage (1-100). Default is 100.
31+
/// </param>
2932
/// <returns>
3033
/// A result containing the simulation ID, actual parameters used, and timing information.
3134
/// </returns>
@@ -43,5 +46,5 @@ public interface ICpuStressService
4346
/// </list>
4447
/// </para>
4548
/// </remarks>
46-
Task<SimulationResult> TriggerCpuStressAsync(int durationSeconds, CancellationToken cancellationToken);
49+
Task<SimulationResult> TriggerCpuStressAsync(int durationSeconds, CancellationToken cancellationToken, int targetPercentage = 100);
4750
}

src/PerfProblemSimulator/wwwroot/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ <h3>🔥 CPU Stress</h3>
4444
<label>Duration (s):
4545
<input type="number" id="cpuDuration" value="10" min="1" title="Duration in seconds (minimum 1 second)">
4646
</label>
47+
<label>Target CPU (%):
48+
<input type="number" id="cpuTarget" value="100" min="1" max="100" title="Target CPU load percentage (1-100)">
49+
</label>
4750
</div>
4851
<button class="btn btn-danger" id="btnTriggerCpu">🔥 Trigger High CPU</button>
4952
</div>

src/PerfProblemSimulator/wwwroot/js/dashboard.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -761,18 +761,22 @@ function stopClientProbe() {
761761

762762
async function triggerCpuStress() {
763763
const duration = parseInt(document.getElementById('cpuDuration').value) || 10;
764+
const target = parseInt(document.getElementById('cpuTarget').value) || 100;
764765

765766
try {
766-
logEvent('info', `Triggering CPU stress for ${duration} seconds...`);
767+
logEvent('info', `Triggering CPU stress for ${duration} seconds @ ${target}%...`);
767768
const response = await fetch(`${CONFIG.apiBaseUrl}/cpu/trigger-high-cpu`, {
768769
method: 'POST',
769770
headers: { 'Content-Type': 'application/json' },
770-
body: JSON.stringify({ durationSeconds: duration })
771+
body: JSON.stringify({
772+
durationSeconds: duration,
773+
targetPercentage: target
774+
})
771775
});
772776

773777
if (response.ok) {
774778
const result = await response.json();
775-
addActiveSimulation(result.simulationId, 'cpu', 'CPU Stress');
779+
addActiveSimulation(result.simulationId, 'cpu', `CPU Stress (${target}%)`);
776780
logEvent('success', `CPU stress started: ${result.simulationId}`);
777781
} else {
778782
const error = await response.json();

0 commit comments

Comments
 (0)