C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
86: Profiling .NET Applications
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:
- Analyze the provided
StockTrackerclass below. - Identify the "Memory Leak" (the reason why objects are staying in memory even after they are no longer needed).
- Rewrite the
UpdatePricemethod or the storage mechanism to ensure that memory is reclaimed. - 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);
}
}There are no comments for now.