Understanding dotTrace Categories: Does Reflection Track Your Code or the Runtime?
When optimizing slow endpoints in .NET applications, JetBrains dotTrace is one of the most powerful profiling tools available. However, interpreting its breakdown of categories can sometimes lead to confusion—especially when dealing with categories like User code, System code, and Reflection.
If you've noticed a tiny number next to "Reflection" (such as 0.3 ms) while your method takes hundreds of milliseconds, you might wonder: Does this represent the reflection I wrote in my own code, or internal framework overhead? And should I focus only on 'User code' to speed things up?
Let’s dive into how dotTrace categorizes execution time, what the Reflection category truly represents, and how to pinpoint the real bottleneck in your code.
What Does the "Reflection" Category in dotTrace Mean?
In JetBrains dotTrace, subsystem categories classify where CPU time is spent based on namespace and assembly heuristics. The Reflection category tracks time spent specifically inside reflection-related methods (such as those under System.Reflection, emitting dynamic assemblies, invoking methods dynamically via MethodInfo.Invoke, or type discovery).
Here is how it works:
- It covers both your code and runtime calls: If your method calls
typeof(T).GetProperties()orpropertyInfo.GetValue(obj), the time CPU spends inside the .NET runtime executing those calls is classified under Reflection. - Your calling code is still User Code: The tiny boundary where your method prepares the call or handles the result is tallied under User code, but the execution of the reflection API itself lands under Reflection (or System code, depending on view groupings).
If dotTrace indicates that Reflection took only 0.3 ms out of a 716 ms execution time, reflection is definitively not your performance problem. Even if you completely eliminate reflection from that method, you will only save ~0.3 ms—a negligible gain when trying to reduce latency from ~700 ms to under a second.
Is "User Code" the Only Category You Can Improve?
A common misconception is that developers can only optimize the time marked as User code. In your profile breakdown:
No, User code is not the only category you can improve. In fact, "System code" is usually where the biggest performance wins hide.
System code represents the time spent executing .NET BCL (Base Class Library) or third-party library methods that your code invoked. For example, if you read an Excel file or serialize a giant JSON payload:
- Iterating through millions of cells, unzipping the OpenXML package, or parsing strings counts as System code or Collections.
- Allocating lots of transient objects triggers the garbage collector, inflating GC Wait.
You optimize System code indirectly by changing how your user code calls it. For instance, replacing an inefficient LINQ chain, using Span<T> to avoid allocations, switching to streaming APIs instead of loading entire files into memory, or batching operations will dramatically slash both System code and GC Wait.
How to Measure Average Execution Time
A single snapshot profiling run can be misleading because of cold starts, JIT compilation, or background system noise. To get accurate, actionable metrics:
1. Use dotTrace Sampling/Tracing with Multiple Invocations
In dotTrace, ensure you record multiple iterations of your Import action. In the Call Tree or Top Methods view:
- Right-click the method and check the Call Count column alongside total time.
- dotTrace will show you the total time divided by the call count, yielding the average execution time per call.
2. Benchmark with BenchmarkDotNet
For micro-optimizing specific methods (like Read() vs an optimized implementation), avoid HTTP controllers and profile directly with BenchmarkDotNet:
[MemoryDiagnoser]
public class ReadBenchmarks
{
private MyService _service;
[GlobalSetup]
public void Setup()
{
_service = new MyService();
}
[Benchmark]
public async Task BenchmarkRead()
{
await _service.Read();
}
}
BenchmarkDotNet automatically calculates warmups, statistical standard deviations, allocations, and average execution time with precision down to the nanosecond.
Summary: Where to Focus Next
- Ignore the 0.3 ms Reflection time: It's doing virtually zero damage to your performance.
- Drill down into System Code (374 ms): Expand the
async Readnode in the dotTrace Call Tree view to see what library methods (e.g., Excel parsers, stream readers, or JSON converters) are hogging execution time. - Address Allocations (53 ms GC Wait): A 50+ ms garbage collection pause in a 700 ms method suggests heavy object creation. Reducing allocations will speed up execution and stabilize response times across concurrent requests.