Skip to content

Commit 7d83758

Browse files
Serg046adamsitnik
andauthored
Issue #1736: Add ExceptionDiagnoser (#2169)
Co-authored-by: Adam Sitnik <[email protected]>
1 parent 0f7eb25 commit 7d83758

File tree

20 files changed

+276
-17
lines changed

20 files changed

+276
-17
lines changed

docs/articles/configs/diagnosers.md

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ The current Diagnosers are:
3737
It is a cross-platform profiler that allows profile .NET code on every platform - Windows, Linux, macOS.
3838
Please see Wojciech Nagórski's [blog post](https://wojciechnagorski.com/2020/04/cross-platform-profiling-.net-code-with-benchmarkdotnet/) for all the details.
3939
- Threading Diagnoser (`ThreadingDiagnoser`) - .NET Core 3.0+ diagnoser that reports some Threading statistics.
40+
- Exception Diagnoser (`ExceptionDiagnoser`) - a diagnoser that reports the frequency of exceptions thrown during the operation.
4041

4142
## Usage
4243

@@ -59,6 +60,7 @@ private class Config : ManualConfig
5960
Add(new InliningDiagnoser());
6061
Add(new EtwProfiler());
6162
Add(ThreadingDiagnoser.Default);
63+
Add(ExceptionDiagnoser.Default);
6264
}
6365
}
6466
```
@@ -72,6 +74,7 @@ You can also use one of the following attributes (apply it on a class that conta
7274
[ConcurrencyVisualizerProfiler]
7375
[NativeMemoryProfiler]
7476
[ThreadingDiagnoser]
77+
[ExceptionDiagnoser]
7578
```
7679

7780
In BenchmarkDotNet, 1kB = 1024B, 1MB = 1024kB, and so on. The column Gen X means number of GC collections per 1000 operations for that generation.
@@ -123,3 +126,5 @@ In BenchmarkDotNet, 1kB = 1024B, 1MB = 1024kB, and so on. The column Gen X means
123126
[!include[IntroNativeMemory](../samples/IntroNativeMemory.md)]
124127

125128
[!include[IntroThreadingDiagnoser](../samples/IntroThreadingDiagnoser.md)]
129+
130+
[!include[IntroExceptionDiagnoser](../samples/IntroExceptionDiagnoser.md)]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
uid: BenchmarkDotNet.Samples.IntroExceptionDiagnoser
3+
---
4+
5+
## Sample: IntroExceptionDiagnoser
6+
7+
The `ExceptionDiagnoser` uses [AppDomain.FirstChanceException](https://learn.microsoft.com/en-us/dotnet/api/system.appdomain.firstchanceexception) API to report:
8+
9+
* Exception frequency: The number of exceptions thrown during the operations divided by the number of operations.
10+
11+
### Source code
12+
13+
[!code-csharp[IntroExceptionDiagnoser.cs](../../../samples/BenchmarkDotNet.Samples/IntroExceptionDiagnoser.cs)]
14+
15+
### Output
16+
17+
| Method | Mean | Error | StdDev | Exception frequency |
18+
|----------------------- |---------:|----------:|----------:|--------------------:|
19+
| ThrowExceptionRandomly | 4.936 us | 0.1542 us | 0.4499 us | 0.1381 |
20+
21+
### Links
22+
23+
* @docs.diagnosers
24+
* The permanent link to this sample: @BenchmarkDotNet.Samples.IntroExceptionDiagnoser
25+
26+
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using BenchmarkDotNet.Attributes;
2+
using System;
3+
4+
namespace BenchmarkDotNet.Samples
5+
{
6+
[ExceptionDiagnoser]
7+
public class IntroExceptionDiagnoser
8+
{
9+
[Benchmark]
10+
public void ThrowExceptionRandomly()
11+
{
12+
try
13+
{
14+
if (new Random().Next(0, 5) > 1)
15+
{
16+
throw new Exception();
17+
}
18+
}
19+
catch { }
20+
}
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using BenchmarkDotNet.Configs;
2+
using BenchmarkDotNet.Diagnosers;
3+
using System;
4+
5+
namespace BenchmarkDotNet.Attributes
6+
{
7+
[AttributeUsage(AttributeTargets.Class)]
8+
public class ExceptionDiagnoserAttribute : Attribute, IConfigSource
9+
{
10+
public IConfig Config { get; }
11+
12+
public ExceptionDiagnoserAttribute() => Config = ManualConfig.CreateEmpty().AddDiagnoser(ExceptionDiagnoser.Default);
13+
}
14+
}

src/BenchmarkDotNet/Columns/Column.cs

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public static class Column
5555
public const string CompletedWorkItems = "Completed Work Items";
5656
public const string LockContentions = "Lock Contentions";
5757
public const string CodeSize = "Code Size";
58+
public const string Exceptions = "Exceptions";
5859

5960
//Characteristics:
6061
public const string Id = "Id";

src/BenchmarkDotNet/Configs/ImmutableConfig.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,11 @@ internal ImmutableConfig(
106106

107107
public bool HasThreadingDiagnoser() => diagnosers.Contains(ThreadingDiagnoser.Default);
108108

109+
public bool HasExceptionDiagnoser() => diagnosers.Contains(ExceptionDiagnoser.Default);
110+
109111
internal bool HasPerfCollectProfiler() => diagnosers.OfType<PerfCollectProfiler>().Any();
110112

111-
public bool HasExtraStatsDiagnoser() => HasMemoryDiagnoser() || HasThreadingDiagnoser();
113+
public bool HasExtraStatsDiagnoser() => HasMemoryDiagnoser() || HasThreadingDiagnoser() || HasExceptionDiagnoser();
112114

113115
public IDiagnoser GetCompositeDiagnoser(BenchmarkCase benchmarkCase, RunMode runMode)
114116
{

src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public class CommandLineOptions
3737
[Option('t', "threading", Required = false, Default = false, HelpText = "Prints threading statistics")]
3838
public bool UseThreadingDiagnoser { get; set; }
3939

40+
[Option("exceptions", Required = false, Default = false, HelpText = "Prints exception statistics")]
41+
public bool UseExceptionDiagnoser { get; set; }
42+
4043
[Option('d', "disasm", Required = false, Default = false, HelpText = "Gets disassembly of benchmarked code")]
4144
public bool UseDisassemblyDiagnoser
4245
{

src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs

+2
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ private static IConfig CreateConfig(CommandLineOptions options, IConfig globalCo
213213
config.AddDiagnoser(MemoryDiagnoser.Default);
214214
if (options.UseThreadingDiagnoser)
215215
config.AddDiagnoser(ThreadingDiagnoser.Default);
216+
if (options.UseExceptionDiagnoser)
217+
config.AddDiagnoser(ExceptionDiagnoser.Default);
216218
if (options.UseDisassemblyDiagnoser)
217219
config.AddDiagnoser(new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig(
218220
maxDepth: options.DisassemblerRecursiveDepth,

src/BenchmarkDotNet/Diagnosers/DiagnoserResults.cs

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public DiagnoserResults(BenchmarkCase benchmarkCase, ExecuteResult executeResult
1515
GcStats = executeResult.GcStats;
1616
ThreadingStats = executeResult.ThreadingStats;
1717
BuildResult = buildResult;
18+
ExceptionFrequency = executeResult.ExceptionFrequency;
1819
}
1920

2021
public BenchmarkCase BenchmarkCase { get; }
@@ -25,6 +26,8 @@ public DiagnoserResults(BenchmarkCase benchmarkCase, ExecuteResult executeResult
2526

2627
public ThreadingStats ThreadingStats { get; }
2728

29+
public double ExceptionFrequency { get; }
30+
2831
public BuildResult BuildResult { get; }
2932
}
3033
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using BenchmarkDotNet.Analysers;
2+
using BenchmarkDotNet.Columns;
3+
using BenchmarkDotNet.Engines;
4+
using BenchmarkDotNet.Exporters;
5+
using BenchmarkDotNet.Loggers;
6+
using BenchmarkDotNet.Reports;
7+
using BenchmarkDotNet.Running;
8+
using BenchmarkDotNet.Validators;
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Linq;
12+
13+
namespace BenchmarkDotNet.Diagnosers
14+
{
15+
public class ExceptionDiagnoser : IDiagnoser
16+
{
17+
public static readonly ExceptionDiagnoser Default = new ExceptionDiagnoser();
18+
19+
private ExceptionDiagnoser() { }
20+
21+
public IEnumerable<string> Ids => new[] { nameof(ExceptionDiagnoser) };
22+
23+
public IEnumerable<IExporter> Exporters => Array.Empty<IExporter>();
24+
25+
public IEnumerable<IAnalyser> Analysers => Array.Empty<IAnalyser>();
26+
27+
public void DisplayResults(ILogger logger) { }
28+
29+
public RunMode GetRunMode(BenchmarkCase benchmarkCase) => RunMode.NoOverhead;
30+
31+
public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { }
32+
33+
public IEnumerable<Metric> ProcessResults(DiagnoserResults results)
34+
{
35+
yield return new Metric(ExceptionsFrequencyMetricDescriptor.Instance, results.ExceptionFrequency);
36+
}
37+
38+
public IEnumerable<ValidationError> Validate(ValidationParameters validationParameters) => Enumerable.Empty<ValidationError>();
39+
40+
private class ExceptionsFrequencyMetricDescriptor : IMetricDescriptor
41+
{
42+
internal static readonly IMetricDescriptor Instance = new ExceptionsFrequencyMetricDescriptor();
43+
44+
public string Id => "ExceptionFrequency";
45+
public string DisplayName => Column.Exceptions;
46+
public string Legend => "Exceptions thrown per single operation";
47+
public string NumberFormat => "#0.0000";
48+
public UnitType UnitType => UnitType.Dimensionless;
49+
public string Unit => "Count";
50+
public bool TheGreaterTheBetter => false;
51+
public int PriorityInCategory => 0;
52+
}
53+
}
54+
}

src/BenchmarkDotNet/Engines/Engine.cs

+10-6
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,16 @@ public RunResults Run()
132132

133133
Host.AfterMainRun();
134134

135-
(GcStats workGcHasDone, ThreadingStats threadingStats) = includeExtraStats
135+
(GcStats workGcHasDone, ThreadingStats threadingStats, double exceptionFrequency) = includeExtraStats
136136
? GetExtraStats(new IterationData(IterationMode.Workload, IterationStage.Actual, 0, invokeCount, UnrollFactor))
137-
: (GcStats.Empty, ThreadingStats.Empty);
137+
: (GcStats.Empty, ThreadingStats.Empty, 0);
138138

139139
if (EngineEventSource.Log.IsEnabled())
140140
EngineEventSource.Log.BenchmarkStop(BenchmarkName);
141141

142142
var outlierMode = TargetJob.ResolveValue(AccuracyMode.OutlierModeCharacteristic, Resolver);
143143

144-
return new RunResults(idle, main, outlierMode, workGcHasDone, threadingStats);
144+
return new RunResults(idle, main, outlierMode, workGcHasDone, threadingStats, exceptionFrequency);
145145
}
146146

147147
public Measurement RunIteration(IterationData data)
@@ -189,7 +189,7 @@ public Measurement RunIteration(IterationData data)
189189
return measurement;
190190
}
191191

192-
private (GcStats, ThreadingStats) GetExtraStats(IterationData data)
192+
private (GcStats, ThreadingStats, double) GetExtraStats(IterationData data)
193193
{
194194
// we enable monitoring after main target run, for this single iteration which is executed at the end
195195
// so even if we enable AppDomain monitoring in separate process
@@ -199,19 +199,23 @@ public Measurement RunIteration(IterationData data)
199199
IterationSetupAction(); // we run iteration setup first, so even if it allocates, it is not included in the results
200200

201201
var initialThreadingStats = ThreadingStats.ReadInitial(); // this method might allocate
202+
var exceptionsStats = new ExceptionsStats(); // allocates
203+
exceptionsStats.StartListening(); // this method might allocate
202204
var initialGcStats = GcStats.ReadInitial();
203205

204206
WorkloadAction(data.InvokeCount / data.UnrollFactor);
205207

208+
exceptionsStats.Stop();
206209
var finalGcStats = GcStats.ReadFinal();
207210
var finalThreadingStats = ThreadingStats.ReadFinal();
208211

209212
IterationCleanupAction(); // we run iteration cleanup after collecting GC stats
210213

211-
GcStats gcStats = (finalGcStats - initialGcStats).WithTotalOperations(data.InvokeCount * OperationsPerInvoke);
214+
var totalOperationsCount = data.InvokeCount * OperationsPerInvoke;
215+
GcStats gcStats = (finalGcStats - initialGcStats).WithTotalOperations(totalOperationsCount);
212216
ThreadingStats threadingStats = (finalThreadingStats - initialThreadingStats).WithTotalOperations(data.InvokeCount * OperationsPerInvoke);
213217

214-
return (gcStats, threadingStats);
218+
return (gcStats, threadingStats, exceptionsStats.ExceptionsCount / (double)totalOperationsCount);
215219
}
216220

217221
[MethodImpl(MethodImplOptions.NoInlining)]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Runtime.ExceptionServices;
3+
4+
namespace BenchmarkDotNet.Engines
5+
{
6+
internal class ExceptionsStats
7+
{
8+
internal const string ResultsLinePrefix = "// Exceptions: ";
9+
10+
internal ulong ExceptionsCount { get; private set; }
11+
12+
public void StartListening()
13+
{
14+
AppDomain.CurrentDomain.FirstChanceException += OnFirstChanceException;
15+
}
16+
17+
public void Stop()
18+
{
19+
AppDomain.CurrentDomain.FirstChanceException -= OnFirstChanceException;
20+
}
21+
22+
private void OnFirstChanceException(object sender, FirstChanceExceptionEventArgs e)
23+
{
24+
ExceptionsCount++;
25+
}
26+
27+
public static string ToOutputLine(double exceptionCount) => $"{ResultsLinePrefix} {exceptionCount}";
28+
29+
public static double Parse(string line)
30+
{
31+
if (!line.StartsWith(ResultsLinePrefix))
32+
throw new NotSupportedException($"Line must start with {ResultsLinePrefix}");
33+
34+
var measurement = line.Remove(0, ResultsLinePrefix.Length);
35+
if (!double.TryParse(measurement, out var exceptionsNumber))
36+
{
37+
throw new NotSupportedException("Invalid string");
38+
}
39+
40+
return exceptionsNumber;
41+
}
42+
}
43+
}

src/BenchmarkDotNet/Engines/RunResults.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,21 @@ public struct RunResults
2323

2424
public ThreadingStats ThreadingStats { get; }
2525

26+
public double ExceptionFrequency { get; }
27+
2628
public RunResults([CanBeNull] IReadOnlyList<Measurement> overhead,
2729
[NotNull] IReadOnlyList<Measurement> workload,
2830
OutlierMode outlierMode,
2931
GcStats gcStats,
30-
ThreadingStats threadingStats)
32+
ThreadingStats threadingStats,
33+
double exceptionFrequency)
3134
{
3235
this.outlierMode = outlierMode;
3336
Overhead = overhead;
3437
Workload = workload;
3538
GCStats = gcStats;
3639
ThreadingStats = threadingStats;
40+
ExceptionFrequency = exceptionFrequency;
3741
}
3842

3943
public IEnumerable<Measurement> GetMeasurements()
@@ -68,6 +72,8 @@ public void Print(TextWriter outWriter)
6872
outWriter.WriteLine(GCStats.ToOutputLine());
6973
if (!ThreadingStats.Equals(ThreadingStats.Empty))
7074
outWriter.WriteLine(ThreadingStats.ToOutputLine());
75+
if (ExceptionFrequency > 0)
76+
outWriter.WriteLine(ExceptionsStats.ToOutputLine(ExceptionFrequency));
7177

7278
outWriter.WriteLine();
7379
}

0 commit comments

Comments
 (0)