1515using Spectre . Console . Rendering ;
1616
1717const int AllocationTypeLimit = 3 ;
18+ const int ExceptionTypeLimit = 3 ;
1819var theme = Theme . Current ;
1920var 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+
30253167void 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