Profiling
VisualMCP profiles a running .NET process with the runtime's own diagnostics channel — EventPipe for traces and counters, ClrMD for heaps and dumps. The tools are native: none of them spawns an external diagnostics CLI.
The shape of a session
profile_processes → find the pid
profile_counters → is it CPU, GC or memory? (cheap, 5–10 s)
profile_collect → capture a trace (cpu / gc / exceptions / threading / async / database / …)
profile_analyze → hotspots, allocation sites, GC, exceptions, contention, timeline
profile_root_cause → WHY that code costs: the loop chain, the LINQ per iteration, the allocation
profile_slowdown → or skip straight to a verdict on why it is slow
Measuring
| Tool | What it produces |
|---|---|
profile_counters | EventCounter time series: CPU, GC heap, allocation rate, thread-pool queue, exceptions, contention — with min/max/mean and the raw points. |
profile_collect | A real .nettrace through EventPipe, with presets that set the right providers and stack walks. |
profile_analyze | CPU hotspots with CPU ms and file:line, top stacks, allocation sites, GC pauses, exceptions with throw sites, lock contention, findings and next steps. |
profile_hotpaths | The fix list: ranked user methods, root-to-leaf hot paths, and the branch points where optimising a caller is the better move. |
profile_root_cause | Each measured site joined with the solution's AST: “two nested loops, a List<Order> per element, ToList() materialising the inner query every iteration”. |
profile_async | The async story: exact await durations (send/stop pairs, not samples), and the state machines still alive when the trace ended. |
profile_database | Commands with text and duration from the data-access EventSources, grouped by normalised shape — which is how the N+1 signature appears. |
profile_heap, profile_allocations | What is on the heap now, and who allocated it, with full stacks. |
profile_stacks, profile_dump | Every thread's stack with contended monitors named; a dump you can analyse repeatedly. |
profile_io, profile_file_io | Where threads block in I/O by category and call site; and who reads and writes which file, how much and how often. |
profile_instrumentation | Exact call counts and inclusive/exclusive times, no sampling: the assembly's IL is rewritten with an injected probe. |
Answering
The verdict tools return one answer, not a data dump. Each names its evidence, the share of the measured window it can explain, and the runners-up that lost.
| Question | Tool | Possible answers |
|---|---|---|
| Why is this process slow? | profile_slowdown | cpu, gc, io-wait, sync-over-async, lock-wait, not-slow |
| Why is memory growing? | profile_memory_growth | growth, shrink, stable, single-snapshot |
| Is the thread pool starved? | profile_threadpool | starved, at-risk, burst, healthy, inconclusive |
| Is this change a regression? | profile_review_change | regression, improvement, unchanged, inconclusive |
The refusals are a feature: profile_review_change will
not judge windows under 500 ms, and a long thread-pool queue alone is
a burst, not starvation. A confident wrong answer is worse than a
refusal.
Recipes
“It is slow.”
profile_slowdown processName=MyApp.Api durationSeconds=15
Then follow the verdict: profile_hotpaths for CPU,
profile_io for blocking call sites,
profile_allocations for GC.
“Which code creates the problem?”
profile_collect processName=MyApp.Api profile=cpu durationSeconds=20
profile_hotlines tracePath=…
profile_root_cause tracePath=… userCodeRoot=C:\src\MyApp
profile_hotlines answers where the CPU goes, one
line; profile_root_cause answers why, naming the
construct that owns the cost.
“What is the async code doing?”
profile_collect profile=async processName=MyApp.Api durationSeconds=30
profile_async tracePath=…
The most valuable line is usually the state machines still alive when the trace ended: a fire-and-forget task, a deadlock, work that outlives the request.
“Which database queries cost the time?”
profile_collect profile=database processName=MyApp.Api durationSeconds=30
profile_database tracePath=… top=20
“Memory keeps growing.”
profile_heap processName=MyApp.Api writeJson=true
# … exercise the workload …
profile_heap processName=MyApp.Api writeJson=true
profile_memory_growth baselinePath=first.json currentPath=second.json
Artifacts and limits
Traces, dumps and reports land under the solution's
.visualMCP/profiling/ folder; reports are Markdown, so
they can be attached to a review. Method names resolve to
file:line when a portable PDB is available —
profile_symbols can download them from a symbol server
first. Worth knowing: the target process must expose a diagnostics
endpoint; sampling is statistical, so measure in seconds and compare
two windows; allocation attribution needs stack walks, which the
gc and diagnose presets request; and
profile_file_io needs Windows and an elevated server,
because a kernel ETW session is privileged.