Understanding Hot Stack Debugging in VS2022

Hot Stack Debugging with VS2022


🔥 What Is a “Hot Stack”?

A hot stack (or hot path) is a sequence of function calls consuming the most CPU during a profiling session. These are identified in Visual Studio using flame icons

🔥 in the Call Tree view.

This stack represents the most frequently sampled code path and typically surfaces the core logic or processing bottlenecks responsible for high resource consumption. Hot stacks often appear during:

  • API endpoints under heavy load
  • LINQ joins and materialization’s
  • EF Core or database interactions
  • JSON serialization or logging routines
  • Middleware chains Understanding the hot stack allows engineers to trace the dominant execution path contributing to performance degradation and identify opportunities to refactor or optimize specific segments.

🔍 Visual Example:

Below is a simplified example of a hot stack path seen in Visual Studio:

WorkerThreadStart

→ ThreadPool.Dispatch

→ Controller.InvokeAsync

→ NotificationService.GetUnreadNotificationsForUser

→ LINQ.ToList()

→ JoinIterator

→ EFCore.MoveNext()

→ SqlDataReader.Read()

This stack shows how CPU time accumulates as execution flows from thread start to an EF Core database read via LINQ joins.

Key Metrics to Watch Understanding the Call Tree columns in Visual Studio 2022’s CPU Profiler is critical for pinpointing bottlenecks:

ColumnDescription
Function NameThe name of the method or function being executed. Shows full namespace.
Total CPUCPU time spent in the function plus all the functions it calls (inclusive).
Self CPUCPU time spent only in this function (exclusive), not in any called methods.
ModuleThe DLL or assembly name where the function resides (e.g., efcore, system.linq).
CategoryThe nature of the operation — IO, Database, Json, Logging, ASP.NET , etc.
How to interpret these:
  • High Total CPU, Low Self CPU
  • Means: This function delegates to expensive children — look deeper into nested calls.
    This is where hot stacks hide!
  • High Self CPUMeans: The function itself is doing work, likely a tight loop, large memory copy, or inefficient algorithm.
    Candidate for algorithm optimization or offloading to async/threaded processing.
  • Watch for functions in:
    • System.Linq.Enumerable.* → In-memory LINQ
    • Microsoft.EntityFrameworkCore.* → Slow DB materialization or tracking
    • System.Text.Json.* → JSON serialization overhead
    • Logger.Log* → Expensive logging during request pipeline

How to Read the Hot Stack The hot stack is the deepest and most expensive path your app takes when handling a request. In Visual Studio:

  • It appears under the Call Tree tab with 🔥 icons
  • It often starts with ThreadPool.Dispatch() or WorkerThreadStart()
  • From there, it descends into your middleware, controllers, services, and LINQ/EF

Step-by-Step Analysis:

  1. Start at the topmost function with a high Total CPU
    Usually w3wp.exe or WorkerThreadStart.
  2. Expand all 🔥 icons down the tree
    This shows the path that consumed the most samples (= most time on CPU).
  3. Identify key framework layers
    • Middleware pipeline (e.g., CorsMiddleware, SwaggerMiddleware)
    • Controller methods (YourApp.Controllers.*)
    • Business services (.Service.)
    • LINQ and EF Core logic
  4. Focus on “hot” function chains
    • If MoveNext() is present → EF query enumerating rows
    • If JoinIterator → In-memory joins happening
    • If ToList() happens before filtering → inefficiency
  5. Look for flat Self CPU spikes
    These may indicate blocking calls, sync file IO, or busy loops.

Identify Bottlenecks

Here’s what to look for: ** High Total CPU but Low Self CPU**

Example:

MyController.GetData() → 30% Total CPU, 1% Self CPU

Means:
The function itself is fast, but something it calls is very expensive (probably a database call or heavy loop).

Expand it to find the real culprit.

High Self CPU

Example:

MyParser.Parse() → 10% Self CPU

Means:

This function itself is expensive, maybe a tight loop, recursion, or CPU-heavy operation. Consider optimizing it directly.

Example Walkthrough

Let’s say you see:

NotificationController.GetUnreadNotificationsForUser() → 30% Total CPU |– LINQ JoinIterator() → 29% Total CPU |– EF Query.MoveNext() → 15% Total CPU That means:

  • Most of the time was spent doing a LINQ Join on a query result.
  • Entity Framework is fetching and joining lots of data.
  • You should consider:
    • Using .AsNoTracking()
    • Avoiding ToList() before the join
    • Optimizing the SQL query
Screenshot 2025-05-19 185639.png

Common Patterns and Fixes Understanding these recurring patterns is critical when interpreting CPU-bound call stacks in Visual Studio Profiler. Below are the most common anti-patterns seen in real-world Azure Web Apps, their causes, and recommended remediations:

PatternCauseFix
JoinIterator, .ToList()LINQ joins evaluated in memory after fetching full result sets from EF Core.Refactor LINQ to SQL-based joins using IQueryable. Avoid .ToList() until the end.
MoveNext, StateManager.Track()EF Core is tracking every row for change detection, even on read-only queries.Add .AsNoTracking() to skip tracking when querying read-only data.
JsonSerializer.Serialize()Large or deeply nested objects are being synchronously serialized.Use System.Text.Json (faster than Newtonsoft.Json) and pre-trim output DTOs.
Logger.LogInformationFrequent or verbose logs inside high-throughput requests.Lower the log level, batch log writes, or use async log sinks (e.g., ILogger.BeginScope).
Troubleshooting Table:
SymptomProfiler PatternKusto Signal
High CPU in LINQJoinIterator, ToList()High duration
EF slownessMoveNext, Track()High result count trace
Logging appears in stackLogger.LogInformationTrace frequency
JSON slownessJsonSerializer.Serialize()N/A

Visual Call Tree Example Use the flame path to trace down the slowest portion of execution.

Top-Level CPU Consumers

FunctionTotal CPU [%]Self CPU [%]Module / Category
w3wp.exe (PID:4112)100.0034.59 %Multiple modules
└─ System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()53.010.05System.Private.CoreLib (IO)
└─ System.Threading.ThreadPoolWorkQueue.Dispatch()52.720.02 %System.Private.CoreLib (IO)
└─ Microsoft.AspNetCore.Server.IIS.Core.IISHttpContext.Execute()45.720.00 %AspNetCore.Server.IIS (ASP)
└─ … deep ASP .NET Core middleware & MVC pipeline …44.41–45.720–0.02 %AspNetCore.Server / Swashbuckle (ASP)
Interpretation:
  • Roughly 53 % of all CPU time is spent just spinning up and dispatching thread‐pool work items.
  • Once you’re on a thread‐pool thread, about 45 % of CPU is then consumed inside the ASP .NET Core request pipeline (authentication, static files, Swagger, MVC routing/controllers).

Framework vs. Your Code If you switch to the Top Functions view, you’ll see which specific methods (including EF Core, Reflection, Serialization) dominate:

FunctionTotal CPU [%]Self CPU [%]Category
Microsoft.EntityFrameworkCore.Internal.DbSetInitializer.InitializeSets(DbContext)16.51 %6.82 %Database
System.DefaultBinder.SelectMethod(…, BindingFlags, ParameterInfo[])4.82 %3.69 %Reflection
System.Collections.Generic.Dictionary`2.Resize(int,bool)2.85 %2.85 %Other
Microsoft.AspNetCore.Server.IIS.NativeMethods.http_flush_response_bytes(…, bool, ref bool)2.45 %2.39 %ASP .NET
EventPipeInterop–>Advapi32.EventWriteTransfer(…)2.11 %2.11 %Logging/Etw
Key insights:
  • EF Core’s InitializeSets (model-initialization, change-tracker setup) alone is chewing up ~16 % of total CPU and ~7 % of self CPU.
  • Reflection (method binding) is another 4.8 %, which likely comes from MVC action-selection or model-binding.
  • Core framework (response flushing, event logging) accounts for another 4–5 %. Hot Path Drill-Down Looking at the expanded call tree (hot-path re-rooted at WorkerThreadStart):
  1. Middleware pipeline (AuthN, StaticFiles, HTTPS Redir, CookiePolicy, Swagger, MVC) accounts for ~44 % of the work.
  2. Controller activation & action execution itself (your code under dynamicClass.lambda_method…) shows up much lower—each action lambda only 2–4 %.
  3. Database model setup (the DbSet-initializer) is likely happening on every request if you’re not re-using the DbContext properly or if you haven’t precompiled your EF model.

Other Observations & Potential Issues

  • ThreadPool Overhead: 50+ % of CPU just dispatching work items suggests very short-lived tasks or high request concurrency. Consider batching or reducing context-switch churn.
  • EF Model Compilation: If you haven’t enabled EF Core’s pre-compiled models , each request will pay that ~16 % tax.
  • Reflection/Binding: Heavy use of reflection in MVC (action selection, parameter binding) can be mitigated by endpoint routing or compiled expression trees.
  • Swagger Middleware: Swashbuckle’s JSON generation runs on every request to the docs endpoint—ensure it’s not enabled in production or cache the JSON.
  • Logging ETW: EventWriteTransfer at ~2 % could point to very chatty telemetry; consider batching or sampling.
Picture1.png

EXAMPLE2… How to Read This Call Tree (Beginner Edition)

Top-Level Overview

  • PID 5952 (w3wp.exe) is your IIS worker process handling the web request.
  • The profiler recorded 2613 CPU samples. Each sample = 1 ms of CPU time, so: 2613 samples ≈ 2.613 seconds of total CPU time

Hot Path = Where CPU Time Is Spent Most

  • Visual Studio highlighted the “hot path” — the most CPU-heavy sequence of calls.
  • It begins from System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart() and drills down through your app, middleware, controller, services, and LINQ.

Step-by-Step Interpretation

1. Controller Action: SMCT.WebAPI.Controllers.NotificationController.GetUnreadNotificationsForUser() → 785 samples (30.04% of total CPU) This means 30% of all CPU time was spent in this controller action — that’s a significant hotspot.

2. Service Logic: NotificationService.GetUnreadNotificationsForUser() → Also 785 samples (30.04%)

This means the controller is basically calling this one service — and the service is where all the work is happening. LINQ Operations: System.Linq.Enumerable+WhereSelectEnumerableIterator<T, T>.ToList() → 771 samples (29.51%)

Then:

1.       System.Linq.Enumerable.JoinIterator() → 771 samples (29.51%) These LINQ methods are:

  • Taking a large result set
  • Running a join in memory
  • Then converting it to a list

2. Entity Framework Core Querying Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable<T>.MoveNext() → 762 samples (29.16%) This is where EF is actually reading data from SQL and moving through the result set row-by-row.

3. SQL Execution (Database Time) Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader() → ~15.5% (around 405 samples) This is the actual SQL command execution. Most of the heavy lifting is in:

  • Reading from the database
  • Materializing objects
  • Then LINQ joins in memory

Where You’re Spending the Most Time

Area% of CPUTime (if 1 ms/sample)What it Means
Controller + Service30.04%~0.785 secHigh-level endpoint logic
LINQ Join + ToList29.51%~0.771 secIn-memory join after DB read
EF Core MoveNext()29.16%~0.762 secIterating through result set
SQL Command Execution~15.5%~0.405 secDatabase time
83ca67c4-3558-4c6a-8c22-a9bee469ffd6.png

Summary You’re doing everything right using the profiler — and here’s what it’s telling you:

InsightRecommendation
Most CPU is spent on LINQ and EF joinsPush logic to SQL, avoid in-memory joins
Controller code is thin, service is heavyFocus optimization in the service method
Database call is expensive, but not the biggest costEF + LINQ materialization is more expensive than the SQL call itself
You’re CPU-bound, not I/O-boundOptimize your C# and EF usage, not the SQL server

Leave a comment