Skip to Content
Course content

86: Profiling .NET Applications

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a professional kitchen during the dinner rush. Orders are piling up, customers are complaining about the wait, and the staff is stressed. You could just yell at everyone to "work faster," but that's useless because you don't know why things are slow. Is the sauté chef overwhelmed? Is the dishwasher failing to provide clean pans fast enough? Or is the ticket printer jamming every ten minutes?

To fix it, you don't guess. You stand back with a stopwatch and a clipboard. You track exactly how many seconds a steak spends on the grill versus how long it sits under the heat lamp. That's profiling. In .NET, profiling is the act of moving from "I think this method is slow" to "I know this specific line of code is consuming 40% of my CPU cycles."

Stopping the Guessing Game

Most developers start by adding Console.WriteLine or Stopwatch calls everywhere. I'll be honest: I did this for years. But that's "manual instrumentation," and it's a nightmare. It litters your code with junk and often changes the timing of the application enough to hide the very bug you're looking for (we call those "Heisenbugs").

Instead, we use a Profiler. Whether you're using the built-in tools in Visual Studio, JetBrains dotTrace, or the command-line dotnet-trace tool, the goal is the same: sampling. The profiler pauses your app thousands of times per second and asks, "Where are you currently executing?" If it asks that question 1,000 times and 700 of those times your app is inside System.Text.RegularExpressions, you've found your bottleneck.

The CPU vs. Memory Tug-of-War

When you open a profiling session, you're usually looking for one of two things: CPU spikes or Memory bloat.

CPU profiling is straightforward—it's about time. You're looking for "Hot Paths." For example, imagine you have a loop processing a million log entries. You might find that a simple string.Split(',').ToList() inside that loop is eating your CPU alive. Why? Because it's not just splitting the string; it's allocating a new list and multiple string objects a million times over. This leads us directly into the second problem: the Garbage Collector (GC).

Memory profiling isn't just about finding a "leak" (where memory is never released). In .NET, it's often about Allocation Pressure. If your code creates thousands of short-lived objects every second, the GC has to run constantly to clean them up. While the GC is running, your application often pauses. If you see "GC spikes" in your profiler, you don't necessarily have a leak; you have a "noisy" application that's making the GC work too hard.

Decoding the Flame Graph

Most modern profilers give you a "Flame Graph." It looks like a series of stacked bars of varying widths. Here is how you actually read one without getting a headache:

  • The X-axis (Width): This represents the percentage of time spent in that function. A wide bar is a "hot" function.
  • The Y-axis (Stack): This shows the call stack. The bar at the bottom is the entry point, and each bar on top of it is a function called by the one below.
  • The Strategy: Look for the widest bars at the top of the stacks. That's where the actual work (or waste) is happening. If you see a huge bar for System.Linq.Enumerable.Where, you know your query is the problem, not the method that called the query.
// Typical "Before" profiling: A slow, allocation-heavy method
public void ProcessData(List<string> lines) {
    foreach (var line in lines) {
        // Profiler would show this as a "Hot Path" due to 
        // massive allocations and string manipulation
        var parts = line.Split(',').Select(p => p.Trim()).ToList(); 
        if (parts[0] == "ERROR") {
            Log(parts[1]);
        }
    }
}

// Typical "After" profiling: Optimized for CPU and Memory
public void ProcessData(List<string> lines) {
    foreach (var line in lines) {
        // Using ReadOnlySpan<char> avoids creating new string objects
        ReadOnlySpan<char> span = line.AsSpan();
        int commaIndex = span.IndexOf(',');
        if (commaIndex != -1) {
            if (span.Slice(0, commaIndex).SequenceEqual("ERROR")) {
                Log(line.Substring(commaIndex + 1));
            }
        }
    }
}

I can't stress this enough: Never optimize before you profile. I've seen developers spend two days rewriting a complex algorithm to be "faster," only to discover via the profiler that the actual bottleneck was a slow database call that the algorithm was waiting on. Don't be that person.




📋 Practical Task

Exercise: Hunting the Memory Leak in the Real-Time Stock Ticker

You have been handed a piece of code for a simulated Stock Ticker. The application is designed to track price updates for various symbols. However, after running for 10 minutes, the RAM usage climbs steadily until the application crashes with an OutOfMemoryException.

Your Task:

  1. Analyze the provided StockTracker class below.
  2. Identify the "Memory Leak" (the reason why objects are staying in memory even after they are no longer needed).
  3. Rewrite the UpdatePrice method or the storage mechanism to ensure that memory is reclaimed.
  4. Explain why your fix prevents the memory growth.
public class StockTracker 
{
    // This dictionary stores a history of every price update for every symbol
    private readonly Dictionary<string, List<decimal>> _priceHistory = new();
    
    // An event that external displays subscribe to
    public event Action<string, decimal> OnPriceUpdated;

    public void UpdatePrice(string symbol, decimal price) 
    {
        if (!_priceHistory.ContainsKey(symbol)) 
        {
            _priceHistory[symbol] = new List<decimal>();
        }
        
        // The leak is happening here. 
        // In a real-world ticker, millions of updates happen per hour.
        _priceHistory[symbol].Add(price); 
        
        OnPriceUpdated?.Invoke(symbol, price);
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.