.NET Performance Testing
Introduction
Performance testing is a critical aspect of software development that ensures your .NET applications run efficiently and can handle expected loads. Unlike functional testing that verifies correctness, performance testing focuses on speed, responsiveness, stability, and resource utilization.
In this guide, we'll explore how to properly test the performance of .NET applications, identify bottlenecks, and implement optimization strategies. Whether you're building a web API, desktop application, or backend service, these techniques will help you deliver high-quality software that performs well under real-world conditions.
Why Performance Testing Matters
Before diving into the technical details, let's understand why performance testing is crucial:
- User Experience: Slow applications frustrate users and can lead to abandonment
- Cost Efficiency: Optimized applications require fewer server resources
- Scalability: Performance testing helps identify how applications behave under increasing load
- Reliability: Well-performing applications tend to be more stable under stress
Key Performance Metrics
When testing .NET applications, focus on these key metrics:
- Response Time: How quickly your application processes a request
- Throughput: Number of operations completed per unit of time
- Resource Utilization: CPU, memory, disk I/O, and network usage
- Garbage Collection: Frequency and duration of GC pauses
- Startup Time: How long your application takes to initialize
Performance Testing Tools for .NET
1. BenchmarkDotNet
BenchmarkDotNet is a powerful .NET library for benchmarking code. It's particularly useful for micro-benchmarking specific methods or algorithms.
To get started with BenchmarkDotNet:
- Install the NuGet package:
dotnet add package BenchmarkDotNet
- Create a benchmark class:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PerformanceTests
{
public class StringOperationsBenchmark
{
private const string TestString = "This is a test string for benchmark";
private readonly List<string> stringList = new List<string> {
"apple", "banana", "cherry", "date", "elderberry"
};
[Benchmark]
public string StringConcatenation()
{
string result = string.Empty;
for (int i = 0; i < 100; i++)
{
result += i.ToString();
}
return result;
}
[Benchmark]
public string StringBuilderConcatenation()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int i = 0; i < 100; i++)
{
sb.Append(i.ToString());
}
return sb.ToString();
}
[Benchmark]
public bool ContainsLinearSearch()
{
return stringList.Contains("cherry");
}
[Benchmark]
public bool ContainsHashSetSearch()
{
var hashSet = new HashSet<string>(stringList);
return hashSet.Contains("cherry");
}
}
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<StringOperationsBenchmark>();
}
}
}
When you run this benchmark, you'll get detailed statistics comparing the performance of each method:
Example Output:
// * Summary *
BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19045.3693)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.400
[Host] : .NET 7.0.10 (7.0.1023.36312), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.10 (7.0.1023.36312), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Median |
|------------------------- |----------:|---------:|---------:|----------:|
| StringConcatenation | 22.532 μs | 0.450 μs | 0.983 μs | 22.264 μs |
| StringBuilderConcatenation | 1.427 μs | 0.028 μs | 0.056 μs | 1.424 μs |
| ContainsLinearSearch | 69.17 ns | 1.40 ns | 1.92 ns | 68.45 ns |
| ContainsHashSetSearch | 33.56 ns | 0.68 ns | 0.89 ns | 33.34 ns |
This output clearly shows that StringBuilder
is significantly faster than string concatenation, and HashSet-based searching outperforms linear searching.
2. dotTrace and dotMemory
JetBrains offers professional profiling tools for .NET applications:
- dotTrace: For CPU profiling and performance analysis
- dotMemory: For memory usage analysis
These tools can integrate with Visual Studio and provide rich visualizations of performance data.
3. Visual Studio Diagnostics Tools
Visual Studio includes built-in performance analysis tools:
- Open your project in Visual Studio
- Click on "Debug" > "Performance Profiler"
- Select the profiling tools you want to use (CPU Usage, Memory Usage, etc.)
- Run your application and analyze the results
4. PerfView
PerfView is a free performance analysis tool from Microsoft that provides deep insights into .NET application behavior.
# Example of using PerfView from command line
PerfView collect -merge:true -zip:true MyApplication.exe
Creating a Performance Test Suite
Let's build a comprehensive performance test suite for a simple web API:
- First, create a test project:
dotnet new xunit -n MyApi.PerformanceTests
dotnet add package BenchmarkDotNet
dotnet add package NBomber
- Create a performance test that measures database access:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Data.SqlClient;
using System.Threading.Tasks;
namespace MyApi.PerformanceTests
{
[MemoryDiagnoser]
public class DatabasePerformanceTests
{
private readonly string _connectionString = "Server=localhost;Database=TestDB;Integrated Security=true";
private SqlConnection _connection;
[GlobalSetup]
public void Setup()
{
_connection = new SqlConnection(_connectionString);
_connection.Open();
}
[GlobalCleanup]
public void Cleanup()
{
_connection?.Close();
_connection?.Dispose();
}
[Benchmark]
public async Task<int> SingleQueryAsync()
{
using var cmd = new SqlCommand("SELECT COUNT(*) FROM Users", _connection);
return (int)await cmd.ExecuteScalarAsync();
}
[Benchmark]
public async Task<int> QueryWithParameterAsync()
{
using var cmd = new SqlCommand("SELECT COUNT(*) FROM Users WHERE CreatedDate > @date", _connection);
cmd.Parameters.AddWithValue("@date", DateTime.Now.AddDays(-30));
return (int)await cmd.ExecuteScalarAsync();
}
}
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<DatabasePerformanceTests>();
}
}
}
Load Testing with NBomber
NBomber is a modern load testing framework for .NET applications:
using NBomber.Contracts;
using NBomber.CSharp;
using NBomber.Plugins.Http.CSharp;
using System;
using System.Threading.Tasks;
namespace MyApi.PerformanceTests
{
public class LoadTests
{
public static void RunApiLoadTest()
{
var httpClient = new HttpClientFactory();
var step = Step.Create("fetch_users", async context =>
{
var request = Http.CreateRequest("GET", "https://myapi.com/api/users")
.WithHeader("Accept", "application/json");
var response = await Http.Send(httpClient, request);
return response.IsSuccessStatusCode
? Response.Ok(statusCode: (int)response.StatusCode)
: Response.Fail(statusCode: (int)response.StatusCode);
});
var scenario = ScenarioBuilder
.CreateScenario("users_api_test", step)
.WithWarmUpDuration(TimeSpan.FromSeconds(5))
.WithLoadSimulations(
Simulation.InjectPerSec(rate: 100, during: TimeSpan.FromSeconds(30))
);
NBomberRunner
.RegisterScenarios(scenario)
.Run();
}
}
}
This test will simulate 100 requests per second to the users API endpoint for 30 seconds.
Memory Leak Detection
Memory leaks can severely impact application performance over time. Here's how to detect them:
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace MyApi.PerformanceTests
{
[TestClass]
public class MemoryLeakTests
{
[TestMethod]
public async Task DetectMemoryLeaks()
{
// Arrange
var process = Process.GetCurrentProcess();
process.GC.Collect(2, GCCollectionMode.Forced);
await Task.Delay(500); // Allow GC to run
long initialMemory = process.WorkingSet64;
// Act
for (int i = 0; i < 1000; i++)
{
// Call the method suspected of leaking memory
MyPotentiallyLeakyMethod();
if (i % 100 == 0)
{
// Force garbage collection every 100 iterations
process.GC.Collect(2, GCCollectionMode.Forced);
await Task.Delay(100);
}
}
process.GC.Collect(2, GCCollectionMode.Forced);
await Task.Delay(500);
// Assert
long finalMemory = process.WorkingSet64;
long memoryDifference = finalMemory - initialMemory;
// If memory usage has significantly increased, we might have a leak
Console.WriteLine($"Memory difference: {memoryDifference / 1024 / 1024} MB");
// Allow for some overhead but flag significant increases
Assert.IsTrue(memoryDifference < 10 * 1024 * 1024, "Possible memory leak detected");
}
private void MyPotentiallyLeakyMethod()
{
// Method you want to test for memory leaks
}
}
}
Performance Best Practices in .NET
As you work on performance testing, keep these best practices in mind:
-
Establish baselines: Always measure performance before and after changes
-
Use realistic data: Test with production-like datasets
-
Test multiple scenarios: Measure both average and worst-case performance
-
Automate performance tests: Include them in your CI/CD pipeline
-
Keep test environments consistent: Use the same configuration for all test runs
-
Focus on user-impacting metrics: Prioritize optimizations that impact user experience
-
Set performance budgets: Define acceptable thresholds for key metrics
Common Performance Issues and Solutions
Issue | Detection | Solution |
---|---|---|
String concatenation in loops | CPU profiling | Use StringBuilder |
N+1 queries | Database profiling | Use eager loading or batch queries |
Memory leaks | Memory profiling | Dispose IDisposable objects, watch for event handlers |
Excessive garbage collection | Performance counters | Reduce object allocation, use object pooling |
Sequential I/O operations | CPU profiling | Use async/await pattern for I/O bound operations |
Real-World Example: Optimizing API Response Time
Let's look at a complete example of identifying and fixing performance issues in a REST API endpoint:
// Original slow implementation
[HttpGet("customers")]
public async Task<ActionResult<List<CustomerDto>>> GetCustomers()
{
var customers = await _context.Customers.ToListAsync();
var orders = await _context.Orders.ToListAsync();
var result = customers.Select(c => new CustomerDto
{
Id = c.Id,
Name = c.Name,
Email = c.Email,
OrderCount = orders.Count(o => o.CustomerId == c.Id)
}).ToList();
return Ok(result);
}
After performance testing, we identify two major issues:
- Loading all orders in memory
- Inefficient counting of orders per customer
Here's the optimized version:
// Optimized implementation
[HttpGet("customers")]
public async Task<ActionResult<List<CustomerDto>>> GetCustomers()
{
// Get customers and order counts in a single query
var customersWithOrderCounts = await _context.Customers
.Select(c => new CustomerDto
{
Id = c.Id,
Name = c.Name,
Email = c.Email,
OrderCount = c.Orders.Count
})
.ToListAsync();
return Ok(customersWithOrderCounts);
}
This optimization:
- Reduces from two database queries to one
- Lets the database handle the counting efficiently
- Avoids loading unnecessary data into memory
Summary
Performance testing is an essential part of .NET application development. By measuring, analyzing, and optimizing your code, you can provide a better user experience, reduce costs, and build more reliable applications.
In this guide, we've covered:
- Key performance metrics to track
- Popular performance testing tools for .NET
- How to write effective benchmarks
- Techniques for identifying memory leaks
- Best practices for maintaining good performance
- Real-world examples of performance optimization
Remember that performance optimization should be data-driven. Always measure before and after making changes to ensure your optimizations are actually improving performance.
Additional Resources
- Microsoft .NET Performance Documentation
- BenchmarkDotNet Official Documentation
- NBomber GitHub Repository
- Advanced .NET Debugging by Mario Hewardt
Exercises
- Create a benchmark comparing LINQ methods (Where, Select, etc.) with traditional loops
- Profile a simple web API using Visual Studio's built-in tools
- Write a load test that simulates 1000 concurrent users accessing your API
- Use dotMemory or a similar tool to identify memory leaks in a sample application
- Optimize a database query that's performing poorly and measure the improvement
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)