Skip to content

Commit e5d9cf3

Browse files
committed
Annotate CPU call tree with thrown exceptions
1 parent 1f5b6da commit e5d9cf3

File tree

2 files changed

+189
-18
lines changed

2 files changed

+189
-18
lines changed

src/ProfileTool/Cpu/CallTreeNode.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ public CallTreeNode(int frameIdx, string name)
2020
public int Calls { get; set; }
2121
public long AllocationBytes { get; set; }
2222
public int AllocationCount { get; set; }
23+
public long ExceptionCount { get; set; }
2324
public Dictionary<string, long>? AllocationByType { get; private set; }
2425
public Dictionary<string, int>? AllocationCountByType { get; private set; }
26+
public Dictionary<string, long>? ExceptionByType { get; private set; }
2527
public Dictionary<int, CallTreeNode> Children { get; } = new();
2628

2729
/// <summary>
@@ -86,4 +88,29 @@ public void AddAllocation(string typeName, long bytes)
8688
AllocationCountByType[typeName] = 1;
8789
}
8890
}
91+
92+
public void AddExceptionTotals(long count)
93+
{
94+
if (count <= 0)
95+
{
96+
return;
97+
}
98+
99+
ExceptionCount += count;
100+
}
101+
102+
public void AddException(string typeName, long count)
103+
{
104+
if (count <= 0)
105+
{
106+
return;
107+
}
108+
109+
AddExceptionTotals(count);
110+
111+
ExceptionByType ??= new Dictionary<string, long>(StringComparer.Ordinal);
112+
ExceptionByType[typeName] = ExceptionByType.TryGetValue(typeName, out var existing)
113+
? existing + count
114+
: count;
115+
}
89116
}

src/ProfileTool/Program.cs

Lines changed: 162 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Spectre.Console.Rendering;
1616

1717
const int AllocationTypeLimit = 3;
18+
const int ExceptionTypeLimit = 3;
1819
var theme = Theme.Current;
1920
var treeGuideStyle = new Style(ParseColor(theme.TreeGuideColor));
2021

@@ -337,7 +338,14 @@ bool EnsureToolAvailable(string toolName, string installHint)
337338
return true;
338339
}
339340

340-
string? CollectCpuMemoryTrace(string[] command, string label)
341+
string BuildExceptionProvider()
342+
{
343+
var keywordsValue = ClrTraceEventParser.Keywords.Exception;
344+
var keywords = ((ulong)keywordsValue).ToString("x", CultureInfo.InvariantCulture);
345+
return $"Microsoft-Windows-DotNETRuntime:0x{keywords}:4";
346+
}
347+
348+
string? CollectCpuTrace(string[] command, string label, bool includeMemory, bool includeException)
341349
{
342350
if (!EnsureToolAvailable("dotnet-trace", DotnetTraceInstall))
343351
{
@@ -346,23 +354,46 @@ bool EnsureToolAvailable(string toolName, string installHint)
346354

347355
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", System.Globalization.CultureInfo.InvariantCulture);
348356
var traceFile = Path.Combine(outputDir, $"{label}_{timestamp}.nettrace");
357+
var traceParts = new List<string> { "CPU" };
358+
if (includeMemory)
359+
{
360+
traceParts.Add("allocation");
361+
}
362+
363+
if (includeException)
364+
{
365+
traceParts.Add("exception");
366+
}
367+
368+
var traceLabel = string.Join(" + ", traceParts);
349369

350370
return AnsiConsole.Status()
351371
.Spinner(Spinner.Known.Dots)
352-
.Start($"Collecting CPU + allocation trace for [{theme.AccentColor}]{label}[/]...", ctx =>
372+
.Start($"Collecting {traceLabel} trace for [{theme.AccentColor}]{label}[/]...", ctx =>
353373
{
354374
ctx.Status("Collecting trace data...");
355-
var collectArgs = new List<string>
375+
var collectArgs = new List<string> { "collect" };
376+
if (includeMemory)
356377
{
357-
"collect",
358-
"--profile",
359-
"gc-verbose",
360-
"--providers",
361-
"Microsoft-DotNETCore-SampleProfiler",
362-
"--output",
363-
traceFile,
364-
"--"
365-
};
378+
collectArgs.Add("--profile");
379+
collectArgs.Add("gc-verbose");
380+
}
381+
382+
var providers = new List<string> { "Microsoft-DotNETCore-SampleProfiler" };
383+
if (includeException)
384+
{
385+
providers.Add(BuildExceptionProvider());
386+
}
387+
388+
if (providers.Count > 0)
389+
{
390+
collectArgs.Add("--providers");
391+
collectArgs.Add(string.Join(",", providers));
392+
}
393+
394+
collectArgs.Add("--output");
395+
collectArgs.Add(traceFile);
396+
collectArgs.Add("--");
366397
collectArgs.AddRange(command);
367398
var (success, _, stderr) = RunProcess("dotnet-trace", collectArgs, timeoutMs: 180000);
368399

@@ -431,6 +462,7 @@ bool EnsureToolAvailable(string toolName, string installHint)
431462
var callTreeTotal = 0d;
432463
var totalSamples = 0L;
433464
double? lastSampleTimeMs = null;
465+
var sawTypedException = false;
434466

435467
var etlxPath = traceFile;
436468
if (traceFile.EndsWith(".nettrace", StringComparison.OrdinalIgnoreCase))
@@ -446,6 +478,54 @@ bool EnsureToolAvailable(string toolName, string installHint)
446478

447479
const string sampleProfilerProvider = "Microsoft-DotNETCore-SampleProfiler";
448480

481+
void RecordException(TraceEvent data)
482+
{
483+
var stack = data.CallStack();
484+
if (stack == null)
485+
{
486+
return;
487+
}
488+
489+
var typeName = GetExceptionTypeName(data);
490+
var frames = EnumerateCpuFrames(stack).ToList();
491+
if (frames.Count == 0)
492+
{
493+
frames.Add("Unknown");
494+
}
495+
496+
frames.Reverse();
497+
498+
var node = callTreeRoot;
499+
node.AddExceptionTotals(1);
500+
for (var i = 0; i < frames.Count; i++)
501+
{
502+
var frame = frames[i];
503+
if (!frameIndices.TryGetValue(frame, out var frameIdx))
504+
{
505+
frameIdx = framesList.Count;
506+
framesList.Add(frame);
507+
frameIndices[frame] = frameIdx;
508+
}
509+
510+
if (!node.Children.TryGetValue(frameIdx, out var child))
511+
{
512+
child = new CallTreeNode(frameIdx, frame);
513+
node.Children[frameIdx] = child;
514+
}
515+
516+
if (i == frames.Count - 1)
517+
{
518+
child.AddException(typeName, 1);
519+
}
520+
else
521+
{
522+
child.AddExceptionTotals(1);
523+
}
524+
525+
node = child;
526+
}
527+
}
528+
449529
source.Clr.GCAllocationTick += data =>
450530
{
451531
var bytes = data.AllocationAmount64;
@@ -500,6 +580,34 @@ bool EnsureToolAvailable(string toolName, string installHint)
500580
}
501581
};
502582

583+
source.Clr.ExceptionStart += data =>
584+
{
585+
sawTypedException = true;
586+
RecordException(data);
587+
};
588+
589+
source.Dynamic.AddCallbackForProviderEvent(
590+
"Microsoft-Windows-DotNETRuntime",
591+
"ExceptionStart",
592+
data =>
593+
{
594+
if (!sawTypedException)
595+
{
596+
RecordException(data);
597+
}
598+
});
599+
600+
source.Dynamic.AddCallbackForProviderEvent(
601+
"Microsoft-Windows-DotNETRuntime",
602+
"ExceptionThrown",
603+
data =>
604+
{
605+
if (!sawTypedException)
606+
{
607+
RecordException(data);
608+
}
609+
});
610+
503611
source.Dynamic.All += data =>
504612
{
505613
if (!string.Equals(data.ProviderName, sampleProfilerProvider, StringComparison.Ordinal))
@@ -653,6 +761,7 @@ void PrintCpuResults(
653761
var countLabel = results.CountLabel;
654762
var countSuffix = results.CountSuffix;
655763
var allocationTypeLimit = results.CallTreeRoot.AllocationBytes > 0 ? AllocationTypeLimit : 0;
764+
var exceptionTypeLimit = results.CallTreeRoot.ExceptionCount > 0 ? ExceptionTypeLimit : 0;
656765

657766
// Top functions overall - using Spectre table
658767
var filteredAll = allFunctions.Where(entry => MatchesFunctionFilter(entry.Name, functionFilter));
@@ -781,6 +890,7 @@ void PrintCpuResults(
781890
timeUnitLabel,
782891
countSuffix,
783892
allocationTypeLimit,
893+
exceptionTypeLimit,
784894
showTimeline,
785895
timelineWidth));
786896
if (showSelfTimeTree)
@@ -797,6 +907,7 @@ void PrintCpuResults(
797907
timeUnitLabel,
798908
countSuffix,
799909
allocationTypeLimit,
910+
exceptionTypeLimit,
800911
showTimeline: false)); // Don't show timeline on self-time tree
801912
}
802913
}
@@ -952,9 +1063,7 @@ void PrintCpuResults(
9521063

9531064
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
9541065
var traceFile = Path.Combine(outputDir, $"{label}_{timestamp}.exc.nettrace");
955-
var keywordsValue = ClrTraceEventParser.Keywords.Exception;
956-
var keywords = ((ulong)keywordsValue).ToString("x", CultureInfo.InvariantCulture);
957-
var provider = $"Microsoft-Windows-DotNETRuntime:0x{keywords}:4";
1066+
var provider = BuildExceptionProvider();
9581067

9591068
return AnsiConsole.Status()
9601069
.Spinner(Spinner.Known.Dots)
@@ -2387,6 +2496,7 @@ IRenderable BuildContentionCallTree(
23872496
siblingCutoffPercent,
23882497
"ms",
23892498
"x",
2499+
0,
23902500
0);
23912501
}
23922502
}
@@ -2644,6 +2754,7 @@ IRenderable BuildCallTree(
26442754
string timeUnitLabel,
26452755
string countSuffix,
26462756
int allocationTypeLimit,
2757+
int exceptionTypeLimit,
26472758
bool showTimeline = false,
26482759
int timelineWidth = 40)
26492760
{
@@ -2740,6 +2851,10 @@ IRenderable BuildCallTree(
27402851
{
27412852
AddAllocationTypeNodes(tree, rootNode, allocationTypeLimit);
27422853
}
2854+
if (exceptionTypeLimit > 0)
2855+
{
2856+
AddExceptionTypeNodes(tree, rootNode, exceptionTypeLimit);
2857+
}
27432858
var children = CallTreeFilters.GetVisibleChildren(
27442859
rootNode,
27452860
includeRuntime,
@@ -2772,6 +2887,10 @@ IRenderable BuildCallTree(
27722887
{
27732888
AddAllocationTypeNodes(childNode, child, allocationTypeLimit);
27742889
}
2890+
if (exceptionTypeLimit > 0)
2891+
{
2892+
AddExceptionTypeNodes(childNode, child, exceptionTypeLimit);
2893+
}
27752894
if (!isSpecialLeaf)
27762895
{
27772896
AddCallTreeChildren(
@@ -2787,6 +2906,7 @@ IRenderable BuildCallTree(
27872906
timeUnitLabel,
27882907
countSuffix,
27892908
allocationTypeLimit,
2909+
exceptionTypeLimit,
27902910
null);
27912911
}
27922912
}
@@ -2935,6 +3055,7 @@ void AddCallTreeChildren(
29353055
string timeUnitLabel,
29363056
string countSuffix,
29373057
int allocationTypeLimit,
3058+
int exceptionTypeLimit,
29383059
TimelineContext? timeline = null)
29393060
{
29403061
if (depth > maxDepth)
@@ -2979,6 +3100,10 @@ void AddCallTreeChildren(
29793100
{
29803101
AddAllocationTypeNodes(childNode, child, allocationTypeLimit);
29813102
}
3103+
if (exceptionTypeLimit > 0)
3104+
{
3105+
AddExceptionTypeNodes(childNode, child, exceptionTypeLimit);
3106+
}
29823107
if (!isSpecialLeaf)
29833108
{
29843109
AddCallTreeChildren(
@@ -2994,6 +3119,7 @@ void AddCallTreeChildren(
29943119
timeUnitLabel,
29953120
countSuffix,
29963121
allocationTypeLimit,
3122+
exceptionTypeLimit,
29973123
timeline);
29983124
}
29993125
}
@@ -3022,6 +3148,22 @@ void AddAllocationTypeNodes(IHasTreeNodes parent, CallTreeNode node, int limit)
30223148
}
30233149
}
30243150

3151+
void AddExceptionTypeNodes(IHasTreeNodes parent, CallTreeNode node, int limit)
3152+
{
3153+
if (limit <= 0 || node.ExceptionByType == null || node.ExceptionByType.Count == 0)
3154+
{
3155+
return;
3156+
}
3157+
3158+
foreach (var entry in node.ExceptionByType.OrderByDescending(kv => kv.Value).Take(limit))
3159+
{
3160+
var typeName = NameFormatter.FormatTypeDisplayName(entry.Key);
3161+
var countText = entry.Value.ToString("N0", CultureInfo.InvariantCulture) + "x";
3162+
var line = $"[{theme.ErrorColor}]{countText}[/] {Markup.Escape(typeName)}";
3163+
parent.AddNode(line);
3164+
}
3165+
}
3166+
30253167
void AddExceptionCallTreeChildren(
30263168
TreeNode parent,
30273169
CallTreeNode node,
@@ -4004,9 +4146,9 @@ void ApplyInputDefaults(
40044146
}
40054147

40064148
string? sharedTraceFile = null;
4007-
if (!hasInput && runCpu && runMemory)
4149+
if (!hasInput && runCpu && (runMemory || runException))
40084150
{
4009-
sharedTraceFile = CollectCpuMemoryTrace(command, label);
4151+
sharedTraceFile = CollectCpuTrace(command, label, runMemory, runException);
40104152
if (sharedTraceFile == null)
40114153
{
40124154
return;
@@ -4109,7 +4251,9 @@ void ApplyInputDefaults(
41094251
Console.WriteLine($"{label} - exception");
41104252
var results = hasInput
41114253
? ExceptionProfileFromInput(inputPath!, label)
4112-
: ExceptionProfileCommand(command, label);
4254+
: sharedTraceFile != null
4255+
? ExceptionProfileFromInput(sharedTraceFile, label)
4256+
: ExceptionProfileCommand(command, label);
41134257
PrintExceptionResults(
41144258
results,
41154259
label,

0 commit comments

Comments
 (0)